mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
Sports news ticker with dynamic headline scrolling (#9)
* Add news manager with RSS feed ticker and dynamic scrolling Co-authored-by: charlesmynard <charlesmynard@gmail.com> * Add F1 feeds, custom feed management script, and comprehensive feed guide Co-authored-by: charlesmynard <charlesmynard@gmail.com> * Remove emoji and improve error/success message formatting Co-authored-by: charlesmynard <charlesmynard@gmail.com> * Add dynamic duration feature for news display with configurable timing Co-authored-by: charlesmynard <charlesmynard@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
245
CUSTOM_FEEDS_GUIDE.md
Normal file
245
CUSTOM_FEEDS_GUIDE.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
# Adding Custom RSS Feeds & Sports - Complete Guide
|
||||||
|
|
||||||
|
This guide shows you **3 different ways** to add custom RSS feeds like F1, MotoGP, or any personal feeds to your news manager.
|
||||||
|
|
||||||
|
## Quick Examples
|
||||||
|
|
||||||
|
### F1 Racing Feeds
|
||||||
|
```bash
|
||||||
|
# BBC F1 (Recommended - works well)
|
||||||
|
python3 add_custom_feed_example.py add "BBC F1" "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
|
||||||
|
|
||||||
|
# Motorsport.com F1
|
||||||
|
python3 add_custom_feed_example.py add "Motorsport F1" "https://www.motorsport.com/rss/f1/news/"
|
||||||
|
|
||||||
|
# Formula1.com Official
|
||||||
|
python3 add_custom_feed_example.py add "F1 Official" "https://www.formula1.com/en/latest/all.xml"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Sports
|
||||||
|
```bash
|
||||||
|
# MotoGP
|
||||||
|
python3 add_custom_feed_example.py add "MotoGP" "https://www.motogp.com/en/rss/news"
|
||||||
|
|
||||||
|
# Tennis
|
||||||
|
python3 add_custom_feed_example.py add "Tennis" "https://www.atptour.com/en/rss/news"
|
||||||
|
|
||||||
|
# Golf
|
||||||
|
python3 add_custom_feed_example.py add "Golf" "https://www.pgatour.com/news.rss"
|
||||||
|
|
||||||
|
# Soccer/Football
|
||||||
|
python3 add_custom_feed_example.py add "ESPN Soccer" "https://www.espn.com/espn/rss/soccer/news"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Personal/Blog Feeds
|
||||||
|
```bash
|
||||||
|
# Personal blog
|
||||||
|
python3 add_custom_feed_example.py add "My Blog" "https://myblog.com/rss.xml"
|
||||||
|
|
||||||
|
# Tech news
|
||||||
|
python3 add_custom_feed_example.py add "TechCrunch" "https://techcrunch.com/feed/"
|
||||||
|
|
||||||
|
# Local news
|
||||||
|
python3 add_custom_feed_example.py add "Local News" "https://localnews.com/rss"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method 1: Command Line (Easiest)
|
||||||
|
|
||||||
|
### Add a Feed
|
||||||
|
```bash
|
||||||
|
python3 add_custom_feed_example.py add "FEED_NAME" "RSS_URL"
|
||||||
|
```
|
||||||
|
|
||||||
|
### List All Feeds
|
||||||
|
```bash
|
||||||
|
python3 add_custom_feed_example.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove a Feed
|
||||||
|
```bash
|
||||||
|
python3 add_custom_feed_example.py remove "FEED_NAME"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Adding F1
|
||||||
|
```bash
|
||||||
|
# Step 1: Check current feeds
|
||||||
|
python3 add_custom_feed_example.py list
|
||||||
|
|
||||||
|
# Step 2: Add BBC F1 feed
|
||||||
|
python3 add_custom_feed_example.py add "BBC F1" "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
|
||||||
|
|
||||||
|
# Step 3: Verify it was added
|
||||||
|
python3 add_custom_feed_example.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method 2: Web Interface
|
||||||
|
|
||||||
|
1. **Open Web Interface**: Go to `http://your-display-ip:5000`
|
||||||
|
2. **Navigate to News Tab**: Click the "News Manager" tab
|
||||||
|
3. **Add Custom Feed**:
|
||||||
|
- Enter feed name in "Feed Name" field (e.g., "BBC F1")
|
||||||
|
- Enter RSS URL in "RSS Feed URL" field
|
||||||
|
- Click "Add Feed" button
|
||||||
|
4. **Enable the Feed**: Check the checkbox next to your new feed
|
||||||
|
5. **Save Settings**: Click "Save News Settings"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Method 3: Direct Config Edit
|
||||||
|
|
||||||
|
Edit `config/config.json` directly:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"news_manager": {
|
||||||
|
"enabled": true,
|
||||||
|
"enabled_feeds": ["NFL", "NCAA FB", "BBC F1"],
|
||||||
|
"custom_feeds": {
|
||||||
|
"BBC F1": "http://feeds.bbci.co.uk/sport/formula1/rss.xml",
|
||||||
|
"Motorsport F1": "https://www.motorsport.com/rss/f1/news/",
|
||||||
|
"My Blog": "https://myblog.com/rss.xml"
|
||||||
|
},
|
||||||
|
"headlines_per_feed": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding RSS Feeds
|
||||||
|
|
||||||
|
### Popular Sports RSS Feeds
|
||||||
|
|
||||||
|
| Sport | Source | RSS URL |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| **F1** | BBC Sport | `http://feeds.bbci.co.uk/sport/formula1/rss.xml` |
|
||||||
|
| **F1** | Motorsport.com | `https://www.motorsport.com/rss/f1/news/` |
|
||||||
|
| **MotoGP** | Official | `https://www.motogp.com/en/rss/news` |
|
||||||
|
| **Tennis** | ATP Tour | `https://www.atptour.com/en/rss/news` |
|
||||||
|
| **Golf** | PGA Tour | `https://www.pgatour.com/news.rss` |
|
||||||
|
| **Soccer** | ESPN | `https://www.espn.com/espn/rss/soccer/news` |
|
||||||
|
| **Boxing** | ESPN | `https://www.espn.com/espn/rss/boxing/news` |
|
||||||
|
| **UFC/MMA** | ESPN | `https://www.espn.com/espn/rss/mma/news` |
|
||||||
|
|
||||||
|
### How to Find RSS Feeds
|
||||||
|
1. **Look for RSS icons** on websites
|
||||||
|
2. **Check `/rss`, `/feed`, or `/rss.xml`** paths
|
||||||
|
3. **Use RSS discovery tools** like RSS Feed Finder
|
||||||
|
4. **Check site footers** for RSS links
|
||||||
|
|
||||||
|
### Testing RSS Feeds
|
||||||
|
```bash
|
||||||
|
# Test if a feed works before adding it
|
||||||
|
python3 -c "
|
||||||
|
import feedparser
|
||||||
|
import requests
|
||||||
|
url = 'YOUR_RSS_URL_HERE'
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
feed = feedparser.parse(response.content)
|
||||||
|
print(f'SUCCESS: Feed works! Title: {feed.feed.get(\"title\", \"N/A\")}')
|
||||||
|
print(f'{len(feed.entries)} articles found')
|
||||||
|
if feed.entries:
|
||||||
|
print(f'Latest: {feed.entries[0].title}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'ERROR: {e}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Configuration
|
||||||
|
|
||||||
|
### Controlling Feed Behavior
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"news_manager": {
|
||||||
|
"headlines_per_feed": 3, // Headlines from each feed
|
||||||
|
"scroll_speed": 2, // Pixels per frame
|
||||||
|
"scroll_delay": 0.02, // Seconds between updates
|
||||||
|
"rotation_enabled": true, // Rotate content to avoid repetition
|
||||||
|
"rotation_threshold": 3, // Cycles before rotating
|
||||||
|
"update_interval": 300 // Seconds between feed updates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feed Priority
|
||||||
|
Feeds are displayed in the order they appear in `enabled_feeds`:
|
||||||
|
```json
|
||||||
|
"enabled_feeds": ["NFL", "BBC F1", "NCAA FB"] // NFL first, then F1, then NCAA
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Display Names
|
||||||
|
You can use any display name for feeds:
|
||||||
|
```bash
|
||||||
|
python3 add_custom_feed_example.py add "Formula 1" "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
|
||||||
|
python3 add_custom_feed_example.py add "Basketball News" "https://www.espn.com/espn/rss/nba/news"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Feed Not Working?
|
||||||
|
1. **Test the RSS URL** using the testing command above
|
||||||
|
2. **Check for HTTPS vs HTTP** - some feeds require secure connections
|
||||||
|
3. **Verify the feed format** - must be valid RSS or Atom
|
||||||
|
4. **Check rate limiting** - some sites block frequent requests
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
- **403 Forbidden**: Site blocks automated requests (try different feed)
|
||||||
|
- **SSL Errors**: Use HTTP instead of HTTPS if available
|
||||||
|
- **No Content**: Feed might be empty or incorrectly formatted
|
||||||
|
- **Slow Loading**: Increase timeout in news manager settings
|
||||||
|
|
||||||
|
### Feed Alternatives
|
||||||
|
If one feed doesn't work, try alternatives:
|
||||||
|
- **ESPN feeds** sometimes have access restrictions
|
||||||
|
- **BBC feeds** are generally reliable
|
||||||
|
- **Official sport websites** often have RSS feeds
|
||||||
|
- **News aggregators** like Google News have topic-specific feeds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Real-World Example: Complete F1 Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. List current setup
|
||||||
|
python3 add_custom_feed_example.py list
|
||||||
|
|
||||||
|
# 2. Add multiple F1 sources for better coverage
|
||||||
|
python3 add_custom_feed_example.py add "BBC F1" "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
|
||||||
|
python3 add_custom_feed_example.py add "Motorsport F1" "https://www.motorsport.com/rss/f1/news/"
|
||||||
|
|
||||||
|
# 3. Add other racing series
|
||||||
|
python3 add_custom_feed_example.py add "MotoGP" "https://www.motogp.com/en/rss/news"
|
||||||
|
|
||||||
|
# 4. Verify all feeds work
|
||||||
|
python3 simple_news_test.py
|
||||||
|
|
||||||
|
# 5. Check final configuration
|
||||||
|
python3 add_custom_feed_example.py list
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: Your display will now rotate between NFL, NCAA FB, BBC F1, Motorsport F1, and MotoGP headlines!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pro Tips
|
||||||
|
|
||||||
|
1. **Start Small**: Add one feed at a time and test it
|
||||||
|
2. **Mix Sources**: Use multiple sources for the same sport for better coverage
|
||||||
|
3. **Monitor Performance**: Too many feeds can slow down updates
|
||||||
|
4. **Use Descriptive Names**: "BBC F1" is better than just "F1"
|
||||||
|
5. **Test Regularly**: RSS feeds can change or break over time
|
||||||
|
6. **Backup Config**: Save your `config.json` before making changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Need help?** The news manager is designed to be flexible and user-friendly. Start with the command line method - it's the easiest way to get started!
|
||||||
177
DYNAMIC_DURATION_GUIDE.md
Normal file
177
DYNAMIC_DURATION_GUIDE.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Dynamic Duration Feature - Complete Guide
|
||||||
|
|
||||||
|
The news manager now includes intelligent **dynamic duration calculation** that automatically determines the exact time needed to display all your selected headlines without cutting off mid-scroll.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Automatic Calculation
|
||||||
|
The system calculates the perfect display duration by:
|
||||||
|
|
||||||
|
1. **Measuring Text Width**: Calculates the exact pixel width of all headlines combined
|
||||||
|
2. **Computing Scroll Distance**: Determines how far text needs to scroll (display width + text width)
|
||||||
|
3. **Calculating Time**: Uses scroll speed and delay to compute exact timing
|
||||||
|
4. **Adding Buffer**: Includes configurable buffer time for smooth transitions
|
||||||
|
5. **Applying Limits**: Ensures duration stays within your min/max preferences
|
||||||
|
|
||||||
|
### Real-World Example
|
||||||
|
With current settings (4 feeds, 2 headlines each):
|
||||||
|
- **Total Headlines**: 8 headlines per cycle
|
||||||
|
- **Estimated Duration**: 57 seconds
|
||||||
|
- **Cycles per Hour**: ~63 cycles
|
||||||
|
- **Result**: Perfect timing, no cut-offs
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Core Settings
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"news_manager": {
|
||||||
|
"dynamic_duration": true, // Enable/disable feature
|
||||||
|
"min_duration": 30, // Minimum display time (seconds)
|
||||||
|
"max_duration": 300, // Maximum display time (seconds)
|
||||||
|
"duration_buffer": 0.1, // Buffer time (10% extra)
|
||||||
|
"headlines_per_feed": 2, // Headlines from each feed
|
||||||
|
"scroll_speed": 2, // Pixels per frame
|
||||||
|
"scroll_delay": 0.02 // Seconds per frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Duration Scenarios
|
||||||
|
|
||||||
|
| Scenario | Headlines | Est. Duration | Cycles/Hour |
|
||||||
|
|----------|-----------|---------------|-------------|
|
||||||
|
| **Light** | 4 headlines | 30s (min) | 120 |
|
||||||
|
| **Medium** | 6 headlines | 30s (min) | 120 |
|
||||||
|
| **Current** | 8 headlines | 57s | 63 |
|
||||||
|
| **Heavy** | 12 headlines | 85s | 42 |
|
||||||
|
| **Maximum** | 20+ headlines | 300s (max) | 12 |
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### Perfect Timing
|
||||||
|
- **No Cut-offs**: Headlines never cut off mid-sentence
|
||||||
|
- **Complete Cycles**: Always shows full rotation of all selected content
|
||||||
|
- **Smooth Transitions**: Buffer time prevents jarring switches
|
||||||
|
|
||||||
|
### Intelligent Scaling
|
||||||
|
- **Adapts to Content**: More feeds = longer duration automatically
|
||||||
|
- **User Control**: Set your preferred min/max limits
|
||||||
|
- **Flexible**: Works with any combination of feeds and headlines
|
||||||
|
|
||||||
|
### Predictable Behavior
|
||||||
|
- **Consistent Experience**: Same content always takes same time
|
||||||
|
- **Reliable Cycling**: Know exactly when content will repeat
|
||||||
|
- **Configurable**: Adjust to your viewing preferences
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Command Line Testing
|
||||||
|
```bash
|
||||||
|
# Test dynamic duration calculations
|
||||||
|
python3 test_dynamic_duration.py
|
||||||
|
|
||||||
|
# Check current status
|
||||||
|
python3 test_dynamic_duration.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Changes
|
||||||
|
```bash
|
||||||
|
# Add more feeds (increases duration)
|
||||||
|
python3 add_custom_feed_example.py add "Tennis" "https://www.atptour.com/en/rss/news"
|
||||||
|
|
||||||
|
# Check new duration
|
||||||
|
python3 test_dynamic_duration.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
1. Go to `http://display-ip:5000`
|
||||||
|
2. Click "News Manager" tab
|
||||||
|
3. Adjust "Duration Settings":
|
||||||
|
- **Min Duration**: Shortest acceptable cycle time
|
||||||
|
- **Max Duration**: Longest acceptable cycle time
|
||||||
|
- **Buffer**: Extra time for smooth transitions
|
||||||
|
|
||||||
|
## Advanced Configuration
|
||||||
|
|
||||||
|
### Fine-Tuning Duration
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"min_duration": 45, // Increase for longer minimum cycles
|
||||||
|
"max_duration": 180, // Decrease for shorter maximum cycles
|
||||||
|
"duration_buffer": 0.15 // Increase buffer for more transition time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scroll Speed Impact
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scroll_speed": 3, // Faster scroll = shorter duration
|
||||||
|
"scroll_delay": 0.015 // Less delay = shorter duration
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Control
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"headlines_per_feed": 3, // More headlines = longer duration
|
||||||
|
"enabled_feeds": [ // More feeds = longer duration
|
||||||
|
"NFL", "NBA", "MLB", "NHL", "BBC F1", "Tennis"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Duration Too Short
|
||||||
|
- **Increase** `min_duration`
|
||||||
|
- **Add** more feeds or headlines per feed
|
||||||
|
- **Decrease** `scroll_speed`
|
||||||
|
|
||||||
|
### Duration Too Long
|
||||||
|
- **Decrease** `max_duration`
|
||||||
|
- **Remove** some feeds
|
||||||
|
- **Reduce** `headlines_per_feed`
|
||||||
|
- **Increase** `scroll_speed`
|
||||||
|
|
||||||
|
### Jerky Transitions
|
||||||
|
- **Increase** `duration_buffer`
|
||||||
|
- **Adjust** `scroll_delay`
|
||||||
|
|
||||||
|
## Disable Dynamic Duration
|
||||||
|
|
||||||
|
To use fixed timing instead:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dynamic_duration": false,
|
||||||
|
"fixed_duration": 60 // Fixed 60-second cycles
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Calculation Formula
|
||||||
|
```
|
||||||
|
total_scroll_distance = display_width + text_width
|
||||||
|
frames_needed = total_scroll_distance / scroll_speed
|
||||||
|
base_time = frames_needed * scroll_delay
|
||||||
|
buffer_time = base_time * duration_buffer
|
||||||
|
final_duration = base_time + buffer_time (within min/max limits)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Display Integration
|
||||||
|
The display controller automatically:
|
||||||
|
1. Calls `news_manager.get_dynamic_duration()`
|
||||||
|
2. Uses returned value for display timing
|
||||||
|
3. Switches to next mode after exact calculated time
|
||||||
|
4. Logs duration decisions for debugging
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Start Conservative**: Use default settings initially
|
||||||
|
2. **Test Changes**: Use test script to preview duration changes
|
||||||
|
3. **Monitor Performance**: Watch for smooth transitions
|
||||||
|
4. **Adjust Gradually**: Make small changes to settings
|
||||||
|
5. **Consider Viewing**: Match duration to your typical viewing patterns
|
||||||
|
|
||||||
|
The dynamic duration feature ensures your news ticker always displays complete, perfectly-timed content cycles regardless of how many feeds or headlines you configure!
|
||||||
245
NEWS_MANAGER_README.md
Normal file
245
NEWS_MANAGER_README.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
# Sports News Manager
|
||||||
|
|
||||||
|
A comprehensive RSS feed ticker system for displaying sports news headlines with dynamic scrolling and intelligent rotation.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🏈 Multiple Sports Feeds
|
||||||
|
- **NFL**: Latest NFL news and updates
|
||||||
|
- **NCAA Football**: College football news
|
||||||
|
- **MLB**: Major League Baseball news
|
||||||
|
- **NBA**: Basketball news and updates
|
||||||
|
- **NHL**: Hockey news
|
||||||
|
- **NCAA Basketball**: College basketball updates
|
||||||
|
- **Big 10**: Big Ten conference news
|
||||||
|
- **Top Sports**: General ESPN sports news
|
||||||
|
- **Custom Feeds**: Add your own RSS feeds
|
||||||
|
|
||||||
|
### 📺 Smart Display Features
|
||||||
|
- **Dynamic Length Detection**: Automatically calculates headline length and adjusts scroll timing
|
||||||
|
- **Perfect Spacing**: Ensures headlines don't cut off mid-text or loop unnecessarily
|
||||||
|
- **Intelligent Rotation**: Prevents repetitive content by rotating through different headlines
|
||||||
|
- **Configurable Speed**: Adjustable scroll speed and timing
|
||||||
|
- **Visual Separators**: Color-coded separators between different news sources
|
||||||
|
|
||||||
|
### ⚙️ Configuration Options
|
||||||
|
- Enable/disable individual sports feeds
|
||||||
|
- Set number of headlines per feed (1-5)
|
||||||
|
- Adjust scroll speed and timing
|
||||||
|
- Configure rotation behavior
|
||||||
|
- Customize fonts and colors
|
||||||
|
- Add custom RSS feeds
|
||||||
|
|
||||||
|
## Default RSS Feeds
|
||||||
|
|
||||||
|
The system comes pre-configured with these ESPN RSS feeds:
|
||||||
|
|
||||||
|
```
|
||||||
|
MLB: http://espn.com/espn/rss/mlb/news
|
||||||
|
NFL: http://espn.go.com/espn/rss/nfl/news
|
||||||
|
NCAA FB: https://www.espn.com/espn/rss/ncf/news
|
||||||
|
NHL: https://www.espn.com/espn/rss/nhl/news
|
||||||
|
NBA: https://www.espn.com/espn/rss/nba/news
|
||||||
|
TOP SPORTS: https://www.espn.com/espn/rss/news
|
||||||
|
BIG10: https://www.espn.com/blog/feed?blog=bigten
|
||||||
|
NCAA: https://www.espn.com/espn/rss/ncaa/news
|
||||||
|
Other: https://www.coveringthecorner.com/rss/current.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Command Line Management
|
||||||
|
|
||||||
|
Use the `enable_news_manager.py` script to manage the news manager:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check current status
|
||||||
|
python3 enable_news_manager.py status
|
||||||
|
|
||||||
|
# Enable news manager
|
||||||
|
python3 enable_news_manager.py enable
|
||||||
|
|
||||||
|
# Disable news manager
|
||||||
|
python3 enable_news_manager.py disable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
|
||||||
|
Access the news manager through the web interface:
|
||||||
|
|
||||||
|
1. Open your browser to `http://your-display-ip:5000`
|
||||||
|
2. Click on the "News Manager" tab
|
||||||
|
3. Configure your preferred settings:
|
||||||
|
- Enable/disable the news manager
|
||||||
|
- Select which sports feeds to display
|
||||||
|
- Set headlines per feed (1-5)
|
||||||
|
- Configure scroll speed and timing
|
||||||
|
- Add custom RSS feeds
|
||||||
|
- Enable/disable rotation
|
||||||
|
|
||||||
|
### Configuration File
|
||||||
|
|
||||||
|
Direct configuration via `config/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"news_manager": {
|
||||||
|
"enabled": true,
|
||||||
|
"update_interval": 300,
|
||||||
|
"scroll_speed": 2,
|
||||||
|
"scroll_delay": 0.02,
|
||||||
|
"headlines_per_feed": 2,
|
||||||
|
"enabled_feeds": ["NFL", "NCAA FB"],
|
||||||
|
"custom_feeds": {
|
||||||
|
"My Team": "https://example.com/rss"
|
||||||
|
},
|
||||||
|
"rotation_enabled": true,
|
||||||
|
"rotation_threshold": 3,
|
||||||
|
"font_size": 12,
|
||||||
|
"font_path": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||||
|
"text_color": [255, 255, 255],
|
||||||
|
"separator_color": [255, 0, 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Dynamic Length Calculation
|
||||||
|
|
||||||
|
The system intelligently calculates the display time for each headline:
|
||||||
|
|
||||||
|
1. **Text Measurement**: Uses PIL to measure the exact pixel width of each headline
|
||||||
|
2. **Scroll Distance**: Calculates total distance needed (text width + display width)
|
||||||
|
3. **Timing Calculation**: Determines exact scroll time based on speed settings
|
||||||
|
4. **Perfect Spacing**: Ensures smooth transitions between headlines
|
||||||
|
|
||||||
|
### Rotation Algorithm
|
||||||
|
|
||||||
|
Prevents repetitive content by:
|
||||||
|
|
||||||
|
1. **Tracking Display Count**: Monitors how many times each headline has been shown
|
||||||
|
2. **Threshold Management**: After a configured number of cycles, rotates to new content
|
||||||
|
3. **Feed Balancing**: Ensures even distribution across selected feeds
|
||||||
|
4. **Freshness**: Prioritizes newer headlines when available
|
||||||
|
|
||||||
|
### Example Calculation
|
||||||
|
|
||||||
|
For a headline "Breaking: Major trade shakes up NFL draft prospects" (51 characters):
|
||||||
|
|
||||||
|
- **Estimated Width**: ~306 pixels (6 pixels per character average)
|
||||||
|
- **Display Width**: 128 pixels
|
||||||
|
- **Total Scroll Distance**: 306 + 128 = 434 pixels
|
||||||
|
- **Scroll Speed**: 2 pixels per frame
|
||||||
|
- **Frame Delay**: 0.02 seconds
|
||||||
|
- **Total Time**: (434 ÷ 2) × 0.02 = 4.34 seconds
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### RSS Feed Test
|
||||||
|
|
||||||
|
Test the RSS feeds directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 simple_news_test.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Test connectivity to ESPN RSS feeds
|
||||||
|
- Parse sample headlines
|
||||||
|
- Calculate scroll timing
|
||||||
|
- Demonstrate rotation logic
|
||||||
|
|
||||||
|
### Integration Test
|
||||||
|
|
||||||
|
Test the full news manager without hardware dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_news_manager.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
The system provides REST API endpoints for external control:
|
||||||
|
|
||||||
|
- `GET /news_manager/status` - Get current status and configuration
|
||||||
|
- `POST /news_manager/update` - Update configuration
|
||||||
|
- `POST /news_manager/refresh` - Force refresh of news data
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **RSS Feed Not Loading**
|
||||||
|
- Check internet connectivity
|
||||||
|
- Verify RSS URL is valid
|
||||||
|
- Check for rate limiting
|
||||||
|
|
||||||
|
2. **Slow Performance**
|
||||||
|
- Reduce number of enabled feeds
|
||||||
|
- Increase update interval
|
||||||
|
- Check network latency
|
||||||
|
|
||||||
|
3. **Text Not Displaying**
|
||||||
|
- Verify font path exists
|
||||||
|
- Check text color settings
|
||||||
|
- Ensure display dimensions are correct
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging by setting the log level:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Adding Custom Feeds
|
||||||
|
|
||||||
|
Add your own RSS feeds through the web interface or configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"custom_feeds": {
|
||||||
|
"My Local Team": "https://myteam.com/rss",
|
||||||
|
"Sports Blog": "https://sportsblog.com/feed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Styling Options
|
||||||
|
|
||||||
|
Customize the appearance:
|
||||||
|
|
||||||
|
- **Font Size**: Adjust text size (8-24 pixels)
|
||||||
|
- **Colors**: RGB values for text and separators
|
||||||
|
- **Font Path**: Use different system fonts
|
||||||
|
- **Scroll Speed**: 1-10 pixels per frame
|
||||||
|
- **Timing**: 0.01-0.1 seconds per frame
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
The news manager is optimized for:
|
||||||
|
|
||||||
|
- **Low Memory Usage**: Efficient caching and cleanup
|
||||||
|
- **Network Efficiency**: Smart update intervals and retry logic
|
||||||
|
- **Smooth Scrolling**: Consistent frame rates
|
||||||
|
- **Fast Loading**: Parallel RSS feed processing
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Planned features:
|
||||||
|
- Breaking news alerts
|
||||||
|
- Team-specific filtering
|
||||||
|
- Score integration
|
||||||
|
- Social media feeds
|
||||||
|
- Voice announcements
|
||||||
|
- Mobile app control
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check the troubleshooting section
|
||||||
|
2. Review the logs for error messages
|
||||||
|
3. Test individual RSS feeds
|
||||||
|
4. Verify configuration settings
|
||||||
162
add_custom_feed_example.py
Normal file
162
add_custom_feed_example.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def add_custom_feed(feed_name, feed_url):
|
||||||
|
"""Add a custom RSS feed to the news manager configuration"""
|
||||||
|
config_path = "config/config.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load current config
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Ensure news_manager section exists
|
||||||
|
if 'news_manager' not in config:
|
||||||
|
print("ERROR: News manager configuration not found!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Add custom feed
|
||||||
|
if 'custom_feeds' not in config['news_manager']:
|
||||||
|
config['news_manager']['custom_feeds'] = {}
|
||||||
|
|
||||||
|
config['news_manager']['custom_feeds'][feed_name] = feed_url
|
||||||
|
|
||||||
|
# Add to enabled feeds if not already there
|
||||||
|
if feed_name not in config['news_manager']['enabled_feeds']:
|
||||||
|
config['news_manager']['enabled_feeds'].append(feed_name)
|
||||||
|
|
||||||
|
# Save updated config
|
||||||
|
with open(config_path, 'w') as f:
|
||||||
|
json.dump(config, f, indent=4)
|
||||||
|
|
||||||
|
print(f"SUCCESS: Successfully added custom feed: {feed_name}")
|
||||||
|
print(f" URL: {feed_url}")
|
||||||
|
print(f" Feed is now enabled and will appear in rotation")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Error adding custom feed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_all_feeds():
|
||||||
|
"""List all available feeds (default + custom)"""
|
||||||
|
config_path = "config/config.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
news_config = config.get('news_manager', {})
|
||||||
|
custom_feeds = news_config.get('custom_feeds', {})
|
||||||
|
enabled_feeds = news_config.get('enabled_feeds', [])
|
||||||
|
|
||||||
|
print("\nAvailable News Feeds:")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Default feeds (hardcoded in news_manager.py)
|
||||||
|
default_feeds = {
|
||||||
|
'MLB': 'http://espn.com/espn/rss/mlb/news',
|
||||||
|
'NFL': 'http://espn.go.com/espn/rss/nfl/news',
|
||||||
|
'NCAA FB': 'https://www.espn.com/espn/rss/ncf/news',
|
||||||
|
'NHL': 'https://www.espn.com/espn/rss/nhl/news',
|
||||||
|
'NBA': 'https://www.espn.com/espn/rss/nba/news',
|
||||||
|
'TOP SPORTS': 'https://www.espn.com/espn/rss/news',
|
||||||
|
'BIG10': 'https://www.espn.com/blog/feed?blog=bigten',
|
||||||
|
'NCAA': 'https://www.espn.com/espn/rss/ncaa/news',
|
||||||
|
'Other': 'https://www.coveringthecorner.com/rss/current.xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\nDefault Sports Feeds:")
|
||||||
|
for name, url in default_feeds.items():
|
||||||
|
status = "ENABLED" if name in enabled_feeds else "DISABLED"
|
||||||
|
print(f" {name}: {status}")
|
||||||
|
print(f" {url}")
|
||||||
|
|
||||||
|
if custom_feeds:
|
||||||
|
print("\nCustom Feeds:")
|
||||||
|
for name, url in custom_feeds.items():
|
||||||
|
status = "ENABLED" if name in enabled_feeds else "DISABLED"
|
||||||
|
print(f" {name}: {status}")
|
||||||
|
print(f" {url}")
|
||||||
|
else:
|
||||||
|
print("\nCustom Feeds: None added yet")
|
||||||
|
|
||||||
|
print(f"\nCurrently Enabled Feeds: {len(enabled_feeds)}")
|
||||||
|
print(f" {', '.join(enabled_feeds)}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Error listing feeds: {e}")
|
||||||
|
|
||||||
|
def remove_custom_feed(feed_name):
|
||||||
|
"""Remove a custom RSS feed"""
|
||||||
|
config_path = "config/config.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
news_config = config.get('news_manager', {})
|
||||||
|
custom_feeds = news_config.get('custom_feeds', {})
|
||||||
|
|
||||||
|
if feed_name not in custom_feeds:
|
||||||
|
print(f"ERROR: Custom feed '{feed_name}' not found!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Remove from custom feeds
|
||||||
|
del config['news_manager']['custom_feeds'][feed_name]
|
||||||
|
|
||||||
|
# Remove from enabled feeds if present
|
||||||
|
if feed_name in config['news_manager']['enabled_feeds']:
|
||||||
|
config['news_manager']['enabled_feeds'].remove(feed_name)
|
||||||
|
|
||||||
|
# Save updated config
|
||||||
|
with open(config_path, 'w') as f:
|
||||||
|
json.dump(config, f, indent=4)
|
||||||
|
|
||||||
|
print(f"SUCCESS: Successfully removed custom feed: {feed_name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Error removing custom feed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage:")
|
||||||
|
print(" python3 add_custom_feed_example.py list")
|
||||||
|
print(" python3 add_custom_feed_example.py add <feed_name> <feed_url>")
|
||||||
|
print(" python3 add_custom_feed_example.py remove <feed_name>")
|
||||||
|
print("\nExamples:")
|
||||||
|
print(" # Add F1 news feed")
|
||||||
|
print(" python3 add_custom_feed_example.py add 'F1' 'https://www.espn.com/espn/rss/rpm/news'")
|
||||||
|
print(" # Add BBC F1 feed")
|
||||||
|
print(" python3 add_custom_feed_example.py add 'BBC F1' 'http://feeds.bbci.co.uk/sport/formula1/rss.xml'")
|
||||||
|
print(" # Add personal blog feed")
|
||||||
|
print(" python3 add_custom_feed_example.py add 'My Blog' 'https://myblog.com/rss.xml'")
|
||||||
|
return
|
||||||
|
|
||||||
|
command = sys.argv[1].lower()
|
||||||
|
|
||||||
|
if command == 'list':
|
||||||
|
list_all_feeds()
|
||||||
|
elif command == 'add':
|
||||||
|
if len(sys.argv) != 4:
|
||||||
|
print("ERROR: Usage: python3 add_custom_feed_example.py add <feed_name> <feed_url>")
|
||||||
|
return
|
||||||
|
feed_name = sys.argv[2]
|
||||||
|
feed_url = sys.argv[3]
|
||||||
|
add_custom_feed(feed_name, feed_url)
|
||||||
|
elif command == 'remove':
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("ERROR: Usage: python3 add_custom_feed_example.py remove <feed_name>")
|
||||||
|
return
|
||||||
|
feed_name = sys.argv[2]
|
||||||
|
remove_custom_feed(feed_name)
|
||||||
|
else:
|
||||||
|
print(f"ERROR: Unknown command: {command}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -70,7 +70,8 @@
|
|||||||
"ncaam_basketball_recent": 30,
|
"ncaam_basketball_recent": 30,
|
||||||
"ncaam_basketball_upcoming": 30,
|
"ncaam_basketball_upcoming": 30,
|
||||||
"music": 30,
|
"music": 30,
|
||||||
"of_the_day": 40
|
"of_the_day": 40,
|
||||||
|
"news_manager": 60
|
||||||
},
|
},
|
||||||
"use_short_date_format": true
|
"use_short_date_format": true
|
||||||
},
|
},
|
||||||
@@ -83,7 +84,7 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"update_interval": 1800,
|
"update_interval": 1800,
|
||||||
"units": "imperial",
|
"units": "imperial",
|
||||||
"display_format": "{temp}°F\n{condition}"
|
"display_format": "{temp}\u00b0F\n{condition}"
|
||||||
},
|
},
|
||||||
"stocks": {
|
"stocks": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
@@ -92,7 +93,13 @@
|
|||||||
"scroll_delay": 0.01,
|
"scroll_delay": 0.01,
|
||||||
"toggle_chart": false,
|
"toggle_chart": false,
|
||||||
"symbols": [
|
"symbols": [
|
||||||
"ASTS", "SCHD", "INTC", "NVDA", "T", "VOO", "SMCI"
|
"ASTS",
|
||||||
|
"SCHD",
|
||||||
|
"INTC",
|
||||||
|
"NVDA",
|
||||||
|
"T",
|
||||||
|
"VOO",
|
||||||
|
"SMCI"
|
||||||
],
|
],
|
||||||
"display_format": "{symbol}: ${price} ({change}%)"
|
"display_format": "{symbol}: ${price} ({change}%)"
|
||||||
},
|
},
|
||||||
@@ -100,7 +107,8 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"update_interval": 600,
|
"update_interval": 600,
|
||||||
"symbols": [
|
"symbols": [
|
||||||
"BTC-USD", "ETH-USD"
|
"BTC-USD",
|
||||||
|
"ETH-USD"
|
||||||
],
|
],
|
||||||
"display_format": "{symbol}: ${price} ({change}%)"
|
"display_format": "{symbol}: ${price} ({change}%)"
|
||||||
},
|
},
|
||||||
@@ -119,7 +127,12 @@
|
|||||||
"max_games_per_league": 5,
|
"max_games_per_league": 5,
|
||||||
"show_odds_only": false,
|
"show_odds_only": false,
|
||||||
"sort_order": "soonest",
|
"sort_order": "soonest",
|
||||||
"enabled_leagues": ["nfl","mlb", "ncaa_fb", "milb"],
|
"enabled_leagues": [
|
||||||
|
"nfl",
|
||||||
|
"mlb",
|
||||||
|
"ncaa_fb",
|
||||||
|
"milb"
|
||||||
|
],
|
||||||
"update_interval": 3600,
|
"update_interval": 3600,
|
||||||
"scroll_speed": 1,
|
"scroll_speed": 1,
|
||||||
"scroll_delay": 0.01,
|
"scroll_delay": 0.01,
|
||||||
@@ -133,7 +146,9 @@
|
|||||||
"token_file": "token.pickle",
|
"token_file": "token.pickle",
|
||||||
"update_interval": 3600,
|
"update_interval": 3600,
|
||||||
"max_events": 3,
|
"max_events": 3,
|
||||||
"calendars": ["birthdays"]
|
"calendars": [
|
||||||
|
"birthdays"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"nhl_scoreboard": {
|
"nhl_scoreboard": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
@@ -146,7 +161,9 @@
|
|||||||
"recent_update_interval": 3600,
|
"recent_update_interval": 3600,
|
||||||
"upcoming_update_interval": 3600,
|
"upcoming_update_interval": 3600,
|
||||||
"show_favorite_teams_only": true,
|
"show_favorite_teams_only": true,
|
||||||
"favorite_teams": ["TB"],
|
"favorite_teams": [
|
||||||
|
"TB"
|
||||||
|
],
|
||||||
"logo_dir": "assets/sports/nhl_logos",
|
"logo_dir": "assets/sports/nhl_logos",
|
||||||
"show_records": true,
|
"show_records": true,
|
||||||
"display_modes": {
|
"display_modes": {
|
||||||
@@ -169,7 +186,9 @@
|
|||||||
"upcoming_update_interval": 3600,
|
"upcoming_update_interval": 3600,
|
||||||
"recent_game_hours": 72,
|
"recent_game_hours": 72,
|
||||||
"show_favorite_teams_only": true,
|
"show_favorite_teams_only": true,
|
||||||
"favorite_teams": ["DAL"],
|
"favorite_teams": [
|
||||||
|
"DAL"
|
||||||
|
],
|
||||||
"logo_dir": "assets/sports/nba_logos",
|
"logo_dir": "assets/sports/nba_logos",
|
||||||
"show_records": true,
|
"show_records": true,
|
||||||
"display_modes": {
|
"display_modes": {
|
||||||
@@ -191,7 +210,10 @@
|
|||||||
"recent_games_to_show": 0,
|
"recent_games_to_show": 0,
|
||||||
"upcoming_games_to_show": 2,
|
"upcoming_games_to_show": 2,
|
||||||
"show_favorite_teams_only": true,
|
"show_favorite_teams_only": true,
|
||||||
"favorite_teams": ["TB", "DAL"],
|
"favorite_teams": [
|
||||||
|
"TB",
|
||||||
|
"DAL"
|
||||||
|
],
|
||||||
"logo_dir": "assets/sports/nfl_logos",
|
"logo_dir": "assets/sports/nfl_logos",
|
||||||
"show_records": true,
|
"show_records": true,
|
||||||
"display_modes": {
|
"display_modes": {
|
||||||
@@ -214,7 +236,10 @@
|
|||||||
"recent_games_to_show": 0,
|
"recent_games_to_show": 0,
|
||||||
"upcoming_games_to_show": 2,
|
"upcoming_games_to_show": 2,
|
||||||
"show_favorite_teams_only": true,
|
"show_favorite_teams_only": true,
|
||||||
"favorite_teams": ["UGA", "AUB"],
|
"favorite_teams": [
|
||||||
|
"UGA",
|
||||||
|
"AUB"
|
||||||
|
],
|
||||||
"logo_dir": "assets/sports/ncaa_fbs_logos",
|
"logo_dir": "assets/sports/ncaa_fbs_logos",
|
||||||
"show_records": true,
|
"show_records": true,
|
||||||
"display_modes": {
|
"display_modes": {
|
||||||
@@ -235,7 +260,10 @@
|
|||||||
"upcoming_update_interval": 3600,
|
"upcoming_update_interval": 3600,
|
||||||
"recent_game_hours": 72,
|
"recent_game_hours": 72,
|
||||||
"show_favorite_teams_only": true,
|
"show_favorite_teams_only": true,
|
||||||
"favorite_teams": ["UGA", "AUB"],
|
"favorite_teams": [
|
||||||
|
"UGA",
|
||||||
|
"AUB"
|
||||||
|
],
|
||||||
"logo_dir": "assets/sports/ncaa_fbs_logos",
|
"logo_dir": "assets/sports/ncaa_fbs_logos",
|
||||||
"show_records": true,
|
"show_records": true,
|
||||||
"display_modes": {
|
"display_modes": {
|
||||||
@@ -254,7 +282,10 @@
|
|||||||
"live_update_interval": 30,
|
"live_update_interval": 30,
|
||||||
"recent_game_hours": 72,
|
"recent_game_hours": 72,
|
||||||
"show_favorite_teams_only": true,
|
"show_favorite_teams_only": true,
|
||||||
"favorite_teams": ["UGA", "AUB"],
|
"favorite_teams": [
|
||||||
|
"UGA",
|
||||||
|
"AUB"
|
||||||
|
],
|
||||||
"logo_dir": "assets/sports/ncaa_fbs_logos",
|
"logo_dir": "assets/sports/ncaa_fbs_logos",
|
||||||
"show_records": true,
|
"show_records": true,
|
||||||
"display_modes": {
|
"display_modes": {
|
||||||
@@ -264,11 +295,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"youtube": {
|
"youtube": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"update_interval": 3600
|
"update_interval": 3600
|
||||||
},
|
},
|
||||||
"mlb": {
|
"mlb": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"live_priority": true,
|
"live_priority": true,
|
||||||
"live_game_duration": 30,
|
"live_game_duration": 30,
|
||||||
"show_odds": true,
|
"show_odds": true,
|
||||||
@@ -282,7 +313,10 @@
|
|||||||
"recent_games_to_show": 1,
|
"recent_games_to_show": 1,
|
||||||
"upcoming_games_to_show": 1,
|
"upcoming_games_to_show": 1,
|
||||||
"show_favorite_teams_only": true,
|
"show_favorite_teams_only": true,
|
||||||
"favorite_teams": ["TB", "TEX"],
|
"favorite_teams": [
|
||||||
|
"TB",
|
||||||
|
"TEX"
|
||||||
|
],
|
||||||
"logo_dir": "assets/sports/mlb_logos",
|
"logo_dir": "assets/sports/mlb_logos",
|
||||||
"show_records": true,
|
"show_records": true,
|
||||||
"display_modes": {
|
"display_modes": {
|
||||||
@@ -292,7 +326,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"milb": {
|
"milb": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"live_priority": true,
|
"live_priority": true,
|
||||||
"live_game_duration": 30,
|
"live_game_duration": 30,
|
||||||
"test_mode": false,
|
"test_mode": false,
|
||||||
@@ -302,7 +336,9 @@
|
|||||||
"upcoming_update_interval": 3600,
|
"upcoming_update_interval": 3600,
|
||||||
"recent_games_to_show": 1,
|
"recent_games_to_show": 1,
|
||||||
"upcoming_games_to_show": 1,
|
"upcoming_games_to_show": 1,
|
||||||
"favorite_teams": ["TAM"],
|
"favorite_teams": [
|
||||||
|
"TAM"
|
||||||
|
],
|
||||||
"logo_dir": "assets/sports/milb_logos",
|
"logo_dir": "assets/sports/milb_logos",
|
||||||
"show_records": true,
|
"show_records": true,
|
||||||
"upcoming_fetch_days": 7,
|
"upcoming_fetch_days": 7,
|
||||||
@@ -315,12 +351,20 @@
|
|||||||
"text_display": {
|
"text_display": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"text": "Subscribe to ChuckBuilds",
|
"text": "Subscribe to ChuckBuilds",
|
||||||
"font_path": "assets/fonts/press-start-2p.ttf",
|
"font_path": "assets/fonts/press-start-2p.ttf",
|
||||||
"font_size": 8,
|
"font_size": 8,
|
||||||
"scroll": true,
|
"scroll": true,
|
||||||
"scroll_speed": 40,
|
"scroll_speed": 40,
|
||||||
"text_color": [255, 0, 0],
|
"text_color": [
|
||||||
"background_color": [0, 0, 0],
|
255,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"background_color": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
"scroll_gap_width": 32
|
"scroll_gap_width": 32
|
||||||
},
|
},
|
||||||
"soccer_scoreboard": {
|
"soccer_scoreboard": {
|
||||||
@@ -335,8 +379,18 @@
|
|||||||
"upcoming_update_interval": 3600,
|
"upcoming_update_interval": 3600,
|
||||||
"recent_game_hours": 168,
|
"recent_game_hours": 168,
|
||||||
"show_favorite_teams_only": true,
|
"show_favorite_teams_only": true,
|
||||||
"favorite_teams": ["LIV"],
|
"favorite_teams": [
|
||||||
"leagues": ["eng.1", "esp.1", "ger.1", "ita.1", "fra.1", "uefa.champions", "usa.1"],
|
"LIV"
|
||||||
|
],
|
||||||
|
"leagues": [
|
||||||
|
"eng.1",
|
||||||
|
"esp.1",
|
||||||
|
"ger.1",
|
||||||
|
"ita.1",
|
||||||
|
"fra.1",
|
||||||
|
"uefa.champions",
|
||||||
|
"usa.1"
|
||||||
|
],
|
||||||
"logo_dir": "assets/sports/soccer_logos",
|
"logo_dir": "assets/sports/soccer_logos",
|
||||||
"show_records": true,
|
"show_records": true,
|
||||||
"display_modes": {
|
"display_modes": {
|
||||||
@@ -356,7 +410,11 @@
|
|||||||
"display_rotate_interval": 20,
|
"display_rotate_interval": 20,
|
||||||
"update_interval": 3600,
|
"update_interval": 3600,
|
||||||
"subtitle_rotate_interval": 10,
|
"subtitle_rotate_interval": 10,
|
||||||
"category_order": ["word_of_the_day", "slovenian_word_of_the_day", "bible_verse_of_the_day"],
|
"category_order": [
|
||||||
|
"word_of_the_day",
|
||||||
|
"slovenian_word_of_the_day",
|
||||||
|
"bible_verse_of_the_day"
|
||||||
|
],
|
||||||
"categories": {
|
"categories": {
|
||||||
"word_of_the_day": {
|
"word_of_the_day": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -374,5 +432,40 @@
|
|||||||
"display_name": "Bible Verse of the Day"
|
"display_name": "Bible Verse of the Day"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"news_manager": {
|
||||||
|
"enabled": true,
|
||||||
|
"update_interval": 300,
|
||||||
|
"scroll_speed": 2,
|
||||||
|
"scroll_delay": 0.02,
|
||||||
|
"headlines_per_feed": 2,
|
||||||
|
"enabled_feeds": [
|
||||||
|
"NFL",
|
||||||
|
"NCAA FB",
|
||||||
|
"F1",
|
||||||
|
"BBC F1"
|
||||||
|
],
|
||||||
|
"custom_feeds": {
|
||||||
|
"F1": "https://www.espn.com/espn/rss/rpm/news",
|
||||||
|
"BBC F1": "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
|
||||||
|
},
|
||||||
|
"rotation_enabled": true,
|
||||||
|
"rotation_threshold": 3,
|
||||||
|
"dynamic_duration": true,
|
||||||
|
"min_duration": 30,
|
||||||
|
"max_duration": 300,
|
||||||
|
"duration_buffer": 0.1,
|
||||||
|
"font_size": 12,
|
||||||
|
"font_path": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||||
|
"text_color": [
|
||||||
|
255,
|
||||||
|
255,
|
||||||
|
255
|
||||||
|
],
|
||||||
|
"separator_color": [
|
||||||
|
255,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
120
enable_news_manager.py
Normal file
120
enable_news_manager.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def enable_news_manager():
|
||||||
|
"""Enable the news manager in the configuration"""
|
||||||
|
config_path = "config/config.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load current config
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Enable news manager
|
||||||
|
if 'news_manager' not in config:
|
||||||
|
print("News manager configuration not found!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
config['news_manager']['enabled'] = True
|
||||||
|
|
||||||
|
# Save updated config
|
||||||
|
with open(config_path, 'w') as f:
|
||||||
|
json.dump(config, f, indent=4)
|
||||||
|
|
||||||
|
print("SUCCESS: News manager enabled successfully!")
|
||||||
|
print(f"Enabled feeds: {config['news_manager']['enabled_feeds']}")
|
||||||
|
print(f"Headlines per feed: {config['news_manager']['headlines_per_feed']}")
|
||||||
|
print(f"Update interval: {config['news_manager']['update_interval']} seconds")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Error enabling news manager: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disable_news_manager():
|
||||||
|
"""Disable the news manager in the configuration"""
|
||||||
|
config_path = "config/config.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load current config
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Disable news manager
|
||||||
|
if 'news_manager' in config:
|
||||||
|
config['news_manager']['enabled'] = False
|
||||||
|
|
||||||
|
# Save updated config
|
||||||
|
with open(config_path, 'w') as f:
|
||||||
|
json.dump(config, f, indent=4)
|
||||||
|
|
||||||
|
print("SUCCESS: News manager disabled successfully!")
|
||||||
|
else:
|
||||||
|
print("News manager configuration not found!")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Error disabling news manager: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def show_status():
|
||||||
|
"""Show current news manager status"""
|
||||||
|
config_path = "config/config.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
if 'news_manager' not in config:
|
||||||
|
print("News manager configuration not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
news_config = config['news_manager']
|
||||||
|
|
||||||
|
print("News Manager Status:")
|
||||||
|
print("=" * 30)
|
||||||
|
print(f"Enabled: {news_config.get('enabled', False)}")
|
||||||
|
print(f"Update Interval: {news_config.get('update_interval', 300)} seconds")
|
||||||
|
print(f"Scroll Speed: {news_config.get('scroll_speed', 2)} pixels/frame")
|
||||||
|
print(f"Scroll Delay: {news_config.get('scroll_delay', 0.02)} seconds/frame")
|
||||||
|
print(f"Headlines per Feed: {news_config.get('headlines_per_feed', 2)}")
|
||||||
|
print(f"Enabled Feeds: {news_config.get('enabled_feeds', [])}")
|
||||||
|
print(f"Rotation Enabled: {news_config.get('rotation_enabled', True)}")
|
||||||
|
print(f"Rotation Threshold: {news_config.get('rotation_threshold', 3)}")
|
||||||
|
print(f"Font Size: {news_config.get('font_size', 12)}")
|
||||||
|
|
||||||
|
custom_feeds = news_config.get('custom_feeds', {})
|
||||||
|
if custom_feeds:
|
||||||
|
print("Custom Feeds:")
|
||||||
|
for name, url in custom_feeds.items():
|
||||||
|
print(f" {name}: {url}")
|
||||||
|
else:
|
||||||
|
print("No custom feeds configured")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Error reading configuration: {e}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python3 enable_news_manager.py [enable|disable|status]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
command = sys.argv[1].lower()
|
||||||
|
|
||||||
|
if command == "enable":
|
||||||
|
enable_news_manager()
|
||||||
|
elif command == "disable":
|
||||||
|
disable_news_manager()
|
||||||
|
elif command == "status":
|
||||||
|
show_status()
|
||||||
|
else:
|
||||||
|
print("Invalid command. Use: enable, disable, or status")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -33,6 +33,7 @@ from src.calendar_manager import CalendarManager
|
|||||||
from src.text_display import TextDisplay
|
from src.text_display import TextDisplay
|
||||||
from src.music_manager import MusicManager
|
from src.music_manager import MusicManager
|
||||||
from src.of_the_day_manager import OfTheDayManager
|
from src.of_the_day_manager import OfTheDayManager
|
||||||
|
from src.news_manager import NewsManager
|
||||||
|
|
||||||
# Get logger without configuring
|
# Get logger without configuring
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -61,9 +62,11 @@ class DisplayController:
|
|||||||
self.youtube = YouTubeDisplay(self.display_manager, self.config) if self.config.get('youtube', {}).get('enabled', False) else None
|
self.youtube = YouTubeDisplay(self.display_manager, self.config) if self.config.get('youtube', {}).get('enabled', False) else None
|
||||||
self.text_display = TextDisplay(self.display_manager, self.config) if self.config.get('text_display', {}).get('enabled', False) else None
|
self.text_display = TextDisplay(self.display_manager, self.config) if self.config.get('text_display', {}).get('enabled', False) else None
|
||||||
self.of_the_day = OfTheDayManager(self.display_manager, self.config) if self.config.get('of_the_day', {}).get('enabled', False) else None
|
self.of_the_day = OfTheDayManager(self.display_manager, self.config) if self.config.get('of_the_day', {}).get('enabled', False) else None
|
||||||
|
self.news_manager = NewsManager(self.config, self.display_manager) if self.config.get('news_manager', {}).get('enabled', False) else None
|
||||||
logger.info(f"Calendar Manager initialized: {'Object' if self.calendar else 'None'}")
|
logger.info(f"Calendar Manager initialized: {'Object' if self.calendar else 'None'}")
|
||||||
logger.info(f"Text Display initialized: {'Object' if self.text_display else 'None'}")
|
logger.info(f"Text Display initialized: {'Object' if self.text_display else 'None'}")
|
||||||
logger.info(f"OfTheDay Manager initialized: {'Object' if self.of_the_day else 'None'}")
|
logger.info(f"OfTheDay Manager initialized: {'Object' if self.of_the_day else 'None'}")
|
||||||
|
logger.info(f"News Manager initialized: {'Object' if self.news_manager else 'None'}")
|
||||||
logger.info("Display modes initialized in %.3f seconds", time.time() - init_time)
|
logger.info("Display modes initialized in %.3f seconds", time.time() - init_time)
|
||||||
|
|
||||||
# Initialize Music Manager
|
# Initialize Music Manager
|
||||||
@@ -255,6 +258,7 @@ class DisplayController:
|
|||||||
if self.youtube: self.available_modes.append('youtube')
|
if self.youtube: self.available_modes.append('youtube')
|
||||||
if self.text_display: self.available_modes.append('text_display')
|
if self.text_display: self.available_modes.append('text_display')
|
||||||
if self.of_the_day: self.available_modes.append('of_the_day')
|
if self.of_the_day: self.available_modes.append('of_the_day')
|
||||||
|
if self.news_manager: self.available_modes.append('news_manager')
|
||||||
if self.music_manager:
|
if self.music_manager:
|
||||||
self.available_modes.append('music')
|
self.available_modes.append('music')
|
||||||
# Add NHL display modes if enabled
|
# Add NHL display modes if enabled
|
||||||
@@ -439,6 +443,17 @@ class DisplayController:
|
|||||||
"""Get the duration for the current display mode."""
|
"""Get the duration for the current display mode."""
|
||||||
mode_key = self.current_display_mode
|
mode_key = self.current_display_mode
|
||||||
|
|
||||||
|
# Handle dynamic duration for news manager
|
||||||
|
if mode_key == 'news_manager' and self.news_manager:
|
||||||
|
try:
|
||||||
|
dynamic_duration = self.news_manager.get_dynamic_duration()
|
||||||
|
logger.info(f"Using dynamic duration for news_manager: {dynamic_duration} seconds")
|
||||||
|
return dynamic_duration
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting dynamic duration for news_manager: {e}")
|
||||||
|
# Fall back to configured duration
|
||||||
|
return self.display_durations.get(mode_key, 60)
|
||||||
|
|
||||||
# Simplify weather key handling
|
# Simplify weather key handling
|
||||||
if mode_key.startswith('weather_'):
|
if mode_key.startswith('weather_'):
|
||||||
return self.display_durations.get(mode_key, 15)
|
return self.display_durations.get(mode_key, 15)
|
||||||
@@ -461,6 +476,7 @@ class DisplayController:
|
|||||||
if self.youtube: self.youtube.update()
|
if self.youtube: self.youtube.update()
|
||||||
if self.text_display: self.text_display.update()
|
if self.text_display: self.text_display.update()
|
||||||
if self.of_the_day: self.of_the_day.update(time.time())
|
if self.of_the_day: self.of_the_day.update(time.time())
|
||||||
|
if self.news_manager: self.news_manager.fetch_news_data()
|
||||||
|
|
||||||
# Update NHL managers
|
# Update NHL managers
|
||||||
if self.nhl_live: self.nhl_live.update()
|
if self.nhl_live: self.nhl_live.update()
|
||||||
@@ -907,6 +923,8 @@ class DisplayController:
|
|||||||
manager_to_display = self.text_display
|
manager_to_display = self.text_display
|
||||||
elif self.current_display_mode == 'of_the_day' and self.of_the_day:
|
elif self.current_display_mode == 'of_the_day' and self.of_the_day:
|
||||||
manager_to_display = self.of_the_day
|
manager_to_display = self.of_the_day
|
||||||
|
elif self.current_display_mode == 'news_manager' and self.news_manager:
|
||||||
|
manager_to_display = self.news_manager
|
||||||
elif self.current_display_mode == 'nhl_recent' and self.nhl_recent:
|
elif self.current_display_mode == 'nhl_recent' and self.nhl_recent:
|
||||||
manager_to_display = self.nhl_recent
|
manager_to_display = self.nhl_recent
|
||||||
elif self.current_display_mode == 'nhl_upcoming' and self.nhl_upcoming:
|
elif self.current_display_mode == 'nhl_upcoming' and self.nhl_upcoming:
|
||||||
@@ -979,6 +997,8 @@ class DisplayController:
|
|||||||
manager_to_display.display() # Assumes internal clearing
|
manager_to_display.display() # Assumes internal clearing
|
||||||
elif self.current_display_mode == 'of_the_day':
|
elif self.current_display_mode == 'of_the_day':
|
||||||
manager_to_display.display(force_clear=self.force_clear)
|
manager_to_display.display(force_clear=self.force_clear)
|
||||||
|
elif self.current_display_mode == 'news_manager':
|
||||||
|
manager_to_display.display_news()
|
||||||
elif self.current_display_mode == 'nfl_live' and self.nfl_live:
|
elif self.current_display_mode == 'nfl_live' and self.nfl_live:
|
||||||
self.nfl_live.display(force_clear=self.force_clear)
|
self.nfl_live.display(force_clear=self.force_clear)
|
||||||
elif self.current_display_mode == 'ncaa_fb_live' and self.ncaa_fb_live:
|
elif self.current_display_mode == 'ncaa_fb_live' and self.ncaa_fb_live:
|
||||||
|
|||||||
488
src/news_manager.py
Normal file
488
src/news_manager.py
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
from typing import Dict, Any, List, Tuple, Optional
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
import re
|
||||||
|
import html
|
||||||
|
from src.config_manager import ConfigManager
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
from src.cache_manager import CacheManager
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class NewsManager:
|
||||||
|
def __init__(self, config: Dict[str, Any], display_manager):
|
||||||
|
self.config = config
|
||||||
|
self.config_manager = ConfigManager()
|
||||||
|
self.display_manager = display_manager
|
||||||
|
self.news_config = config.get('news_manager', {})
|
||||||
|
self.last_update = 0
|
||||||
|
self.news_data = {}
|
||||||
|
self.current_headline_index = 0
|
||||||
|
self.scroll_position = 0
|
||||||
|
self.cached_text_image = None
|
||||||
|
self.cached_text = None
|
||||||
|
self.cache_manager = CacheManager()
|
||||||
|
self.current_headlines = []
|
||||||
|
self.headline_start_times = []
|
||||||
|
self.total_scroll_width = 0
|
||||||
|
self.headlines_displayed = set() # Track displayed headlines for rotation
|
||||||
|
self.dynamic_duration = 60 # Default duration in seconds
|
||||||
|
|
||||||
|
# Default RSS feeds
|
||||||
|
self.default_feeds = {
|
||||||
|
'MLB': 'http://espn.com/espn/rss/mlb/news',
|
||||||
|
'NFL': 'http://espn.go.com/espn/rss/nfl/news',
|
||||||
|
'NCAA FB': 'https://www.espn.com/espn/rss/ncf/news',
|
||||||
|
'NHL': 'https://www.espn.com/espn/rss/nhl/news',
|
||||||
|
'NBA': 'https://www.espn.com/espn/rss/nba/news',
|
||||||
|
'TOP SPORTS': 'https://www.espn.com/espn/rss/news',
|
||||||
|
'BIG10': 'https://www.espn.com/blog/feed?blog=bigten',
|
||||||
|
'NCAA': 'https://www.espn.com/espn/rss/ncaa/news',
|
||||||
|
'Other': 'https://www.coveringthecorner.com/rss/current.xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get scroll settings from config
|
||||||
|
self.scroll_speed = self.news_config.get('scroll_speed', 2)
|
||||||
|
self.scroll_delay = self.news_config.get('scroll_delay', 0.02)
|
||||||
|
self.update_interval = self.news_config.get('update_interval', 300) # 5 minutes
|
||||||
|
|
||||||
|
# Get headline settings from config
|
||||||
|
self.headlines_per_feed = self.news_config.get('headlines_per_feed', 2)
|
||||||
|
self.enabled_feeds = self.news_config.get('enabled_feeds', ['NFL', 'NCAA FB'])
|
||||||
|
self.custom_feeds = self.news_config.get('custom_feeds', {})
|
||||||
|
|
||||||
|
# Rotation settings
|
||||||
|
self.rotation_enabled = self.news_config.get('rotation_enabled', True)
|
||||||
|
self.rotation_threshold = self.news_config.get('rotation_threshold', 3) # After 3 full cycles
|
||||||
|
self.rotation_count = 0
|
||||||
|
|
||||||
|
# Dynamic duration settings
|
||||||
|
self.dynamic_duration_enabled = self.news_config.get('dynamic_duration', True)
|
||||||
|
self.min_duration = self.news_config.get('min_duration', 30)
|
||||||
|
self.max_duration = self.news_config.get('max_duration', 300)
|
||||||
|
self.duration_buffer = self.news_config.get('duration_buffer', 0.1)
|
||||||
|
|
||||||
|
# Font settings
|
||||||
|
self.font_size = self.news_config.get('font_size', 12)
|
||||||
|
self.font_path = self.news_config.get('font_path', '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf')
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
self.text_color = tuple(self.news_config.get('text_color', [255, 255, 255]))
|
||||||
|
self.separator_color = tuple(self.news_config.get('separator_color', [255, 0, 0]))
|
||||||
|
|
||||||
|
# Initialize session with retry strategy
|
||||||
|
self.session = requests.Session()
|
||||||
|
retry_strategy = Retry(
|
||||||
|
total=3,
|
||||||
|
backoff_factor=1,
|
||||||
|
status_forcelist=[429, 500, 502, 503, 504],
|
||||||
|
)
|
||||||
|
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||||
|
self.session.mount("http://", adapter)
|
||||||
|
self.session.mount("https://", adapter)
|
||||||
|
|
||||||
|
logger.info(f"NewsManager initialized with feeds: {self.enabled_feeds}")
|
||||||
|
logger.info(f"Headlines per feed: {self.headlines_per_feed}")
|
||||||
|
logger.info(f"Scroll settings - Speed: {self.scroll_speed} pixels/frame, Delay: {self.scroll_delay*1000:.2f}ms")
|
||||||
|
|
||||||
|
def parse_rss_feed(self, url: str, feed_name: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Parse RSS feed and return list of headlines"""
|
||||||
|
try:
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.get(url, headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
root = ET.fromstring(response.content)
|
||||||
|
headlines = []
|
||||||
|
|
||||||
|
# Handle different RSS formats
|
||||||
|
items = root.findall('.//item')
|
||||||
|
if not items:
|
||||||
|
items = root.findall('.//entry') # Atom feed format
|
||||||
|
|
||||||
|
for item in items[:self.headlines_per_feed * 2]: # Get extra to allow for filtering
|
||||||
|
title_elem = item.find('title')
|
||||||
|
if title_elem is not None:
|
||||||
|
title = html.unescape(title_elem.text or '').strip()
|
||||||
|
|
||||||
|
# Clean up title
|
||||||
|
title = re.sub(r'<[^>]+>', '', title) # Remove HTML tags
|
||||||
|
title = re.sub(r'\s+', ' ', title) # Normalize whitespace
|
||||||
|
|
||||||
|
if title and len(title) > 10: # Filter out very short titles
|
||||||
|
pub_date_elem = item.find('pubDate')
|
||||||
|
if pub_date_elem is None:
|
||||||
|
pub_date_elem = item.find('published') # Atom format
|
||||||
|
|
||||||
|
pub_date = pub_date_elem.text if pub_date_elem is not None else None
|
||||||
|
|
||||||
|
headlines.append({
|
||||||
|
'title': title,
|
||||||
|
'feed': feed_name,
|
||||||
|
'pub_date': pub_date,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"Parsed {len(headlines)} headlines from {feed_name}")
|
||||||
|
return headlines[:self.headlines_per_feed]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing RSS feed {feed_name} ({url}): {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def fetch_news_data(self):
|
||||||
|
"""Fetch news from all enabled feeds"""
|
||||||
|
try:
|
||||||
|
all_headlines = []
|
||||||
|
|
||||||
|
# Combine default and custom feeds
|
||||||
|
all_feeds = {**self.default_feeds, **self.custom_feeds}
|
||||||
|
|
||||||
|
for feed_name in self.enabled_feeds:
|
||||||
|
if feed_name in all_feeds:
|
||||||
|
url = all_feeds[feed_name]
|
||||||
|
headlines = self.parse_rss_feed(url, feed_name)
|
||||||
|
all_headlines.extend(headlines)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Feed '{feed_name}' not found in available feeds")
|
||||||
|
|
||||||
|
# Store headlines by feed for rotation management
|
||||||
|
self.news_data = {}
|
||||||
|
for headline in all_headlines:
|
||||||
|
feed = headline['feed']
|
||||||
|
if feed not in self.news_data:
|
||||||
|
self.news_data[feed] = []
|
||||||
|
self.news_data[feed].append(headline)
|
||||||
|
|
||||||
|
# Prepare current headlines for display
|
||||||
|
self.prepare_headlines_for_display()
|
||||||
|
|
||||||
|
self.last_update = time.time()
|
||||||
|
logger.info(f"Fetched {len(all_headlines)} total headlines from {len(self.enabled_feeds)} feeds")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching news data: {e}")
|
||||||
|
|
||||||
|
def prepare_headlines_for_display(self):
|
||||||
|
"""Prepare headlines for scrolling display with rotation"""
|
||||||
|
if not self.news_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get headlines for display, applying rotation if enabled
|
||||||
|
display_headlines = []
|
||||||
|
|
||||||
|
for feed_name in self.enabled_feeds:
|
||||||
|
if feed_name in self.news_data:
|
||||||
|
feed_headlines = self.news_data[feed_name]
|
||||||
|
|
||||||
|
if self.rotation_enabled and len(feed_headlines) > self.headlines_per_feed:
|
||||||
|
# Rotate headlines to show different ones
|
||||||
|
start_idx = (self.rotation_count * self.headlines_per_feed) % len(feed_headlines)
|
||||||
|
selected = []
|
||||||
|
for i in range(self.headlines_per_feed):
|
||||||
|
idx = (start_idx + i) % len(feed_headlines)
|
||||||
|
selected.append(feed_headlines[idx])
|
||||||
|
display_headlines.extend(selected)
|
||||||
|
else:
|
||||||
|
display_headlines.extend(feed_headlines[:self.headlines_per_feed])
|
||||||
|
|
||||||
|
# Create scrolling text with separators
|
||||||
|
if display_headlines:
|
||||||
|
text_parts = []
|
||||||
|
for i, headline in enumerate(display_headlines):
|
||||||
|
feed_prefix = f"[{headline['feed']}] "
|
||||||
|
text_parts.append(feed_prefix + headline['title'])
|
||||||
|
|
||||||
|
# Join with separators and add spacing
|
||||||
|
separator = " • "
|
||||||
|
self.cached_text = separator.join(text_parts) + " • " # Add separator at end for smooth loop
|
||||||
|
|
||||||
|
# Calculate text dimensions for perfect scrolling
|
||||||
|
self.calculate_scroll_dimensions()
|
||||||
|
|
||||||
|
self.current_headlines = display_headlines
|
||||||
|
logger.info(f"Prepared {len(display_headlines)} headlines for display")
|
||||||
|
|
||||||
|
def calculate_scroll_dimensions(self):
|
||||||
|
"""Calculate exact dimensions needed for smooth scrolling"""
|
||||||
|
if not self.cached_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load font
|
||||||
|
try:
|
||||||
|
font = ImageFont.truetype(self.font_path, self.font_size)
|
||||||
|
except:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
# Calculate text width
|
||||||
|
temp_img = Image.new('RGB', (1, 1))
|
||||||
|
temp_draw = ImageDraw.Draw(temp_img)
|
||||||
|
|
||||||
|
# Get text dimensions
|
||||||
|
bbox = temp_draw.textbbox((0, 0), self.cached_text, font=font)
|
||||||
|
self.total_scroll_width = bbox[2] - bbox[0]
|
||||||
|
|
||||||
|
# Calculate dynamic display duration
|
||||||
|
self.calculate_dynamic_duration()
|
||||||
|
|
||||||
|
logger.info(f"Text width calculated: {self.total_scroll_width} pixels")
|
||||||
|
logger.info(f"Dynamic duration calculated: {self.dynamic_duration} seconds")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calculating scroll dimensions: {e}")
|
||||||
|
self.total_scroll_width = len(self.cached_text) * 8 # Fallback estimate
|
||||||
|
self.calculate_dynamic_duration()
|
||||||
|
|
||||||
|
def calculate_dynamic_duration(self):
|
||||||
|
"""Calculate the exact time needed to display all headlines"""
|
||||||
|
# If dynamic duration is disabled, use fixed duration from config
|
||||||
|
if not self.dynamic_duration_enabled:
|
||||||
|
self.dynamic_duration = self.news_config.get('fixed_duration', 60)
|
||||||
|
logger.info(f"Dynamic duration disabled, using fixed duration: {self.dynamic_duration}s")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.total_scroll_width:
|
||||||
|
self.dynamic_duration = self.min_duration # Use configured minimum
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get display width (assume full width of display)
|
||||||
|
display_width = getattr(self.display_manager, 'width', 128) # Default to 128 if not available
|
||||||
|
|
||||||
|
# Calculate total scroll distance needed
|
||||||
|
# Text needs to scroll from right edge to completely off left edge
|
||||||
|
total_scroll_distance = display_width + self.total_scroll_width
|
||||||
|
|
||||||
|
# Calculate time based on scroll speed and delay
|
||||||
|
# scroll_speed = pixels per frame, scroll_delay = seconds per frame
|
||||||
|
frames_needed = total_scroll_distance / self.scroll_speed
|
||||||
|
total_time = frames_needed * self.scroll_delay
|
||||||
|
|
||||||
|
# Add buffer time for smooth cycling (configurable %)
|
||||||
|
buffer_time = total_time * self.duration_buffer
|
||||||
|
calculated_duration = int(total_time + buffer_time)
|
||||||
|
|
||||||
|
# Apply configured min/max limits
|
||||||
|
if calculated_duration < self.min_duration:
|
||||||
|
self.dynamic_duration = self.min_duration
|
||||||
|
logger.info(f"Duration capped to minimum: {self.min_duration}s")
|
||||||
|
elif calculated_duration > self.max_duration:
|
||||||
|
self.dynamic_duration = self.max_duration
|
||||||
|
logger.info(f"Duration capped to maximum: {self.max_duration}s")
|
||||||
|
else:
|
||||||
|
self.dynamic_duration = calculated_duration
|
||||||
|
|
||||||
|
logger.info(f"Dynamic duration calculation:")
|
||||||
|
logger.info(f" Display width: {display_width}px")
|
||||||
|
logger.info(f" Text width: {self.total_scroll_width}px")
|
||||||
|
logger.info(f" Total scroll distance: {total_scroll_distance}px")
|
||||||
|
logger.info(f" Frames needed: {frames_needed:.1f}")
|
||||||
|
logger.info(f" Base time: {total_time:.2f}s")
|
||||||
|
logger.info(f" Buffer time: {buffer_time:.2f}s ({self.duration_buffer*100}%)")
|
||||||
|
logger.info(f" Calculated duration: {calculated_duration}s")
|
||||||
|
logger.info(f" Final duration: {self.dynamic_duration}s")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calculating dynamic duration: {e}")
|
||||||
|
self.dynamic_duration = self.min_duration # Use configured minimum as fallback
|
||||||
|
|
||||||
|
def should_update(self) -> bool:
|
||||||
|
"""Check if news data should be updated"""
|
||||||
|
return (time.time() - self.last_update) > self.update_interval
|
||||||
|
|
||||||
|
def get_news_display(self) -> Image.Image:
|
||||||
|
"""Generate the scrolling news ticker display"""
|
||||||
|
try:
|
||||||
|
# Update news if needed
|
||||||
|
if self.should_update() or not self.current_headlines:
|
||||||
|
self.fetch_news_data()
|
||||||
|
|
||||||
|
if not self.cached_text:
|
||||||
|
return self.create_no_news_image()
|
||||||
|
|
||||||
|
# Create display image
|
||||||
|
width = self.display_manager.width
|
||||||
|
height = self.display_manager.height
|
||||||
|
|
||||||
|
img = Image.new('RGB', (width, height), (0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Load font
|
||||||
|
try:
|
||||||
|
font = ImageFont.truetype(self.font_path, self.font_size)
|
||||||
|
except:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
# Calculate vertical position (center the text)
|
||||||
|
text_height = self.font_size
|
||||||
|
y_pos = (height - text_height) // 2
|
||||||
|
|
||||||
|
# Calculate scroll position for smooth animation
|
||||||
|
if self.total_scroll_width > 0:
|
||||||
|
# Scroll from right to left
|
||||||
|
x_pos = width - self.scroll_position
|
||||||
|
|
||||||
|
# Draw the text
|
||||||
|
draw.text((x_pos, y_pos), self.cached_text, font=font, fill=self.text_color)
|
||||||
|
|
||||||
|
# If text has scrolled partially off screen, draw it again for seamless loop
|
||||||
|
if x_pos + self.total_scroll_width < width:
|
||||||
|
draw.text((x_pos + self.total_scroll_width, y_pos), self.cached_text, font=font, fill=self.text_color)
|
||||||
|
|
||||||
|
# Update scroll position
|
||||||
|
self.scroll_position += self.scroll_speed
|
||||||
|
|
||||||
|
# Reset scroll when text has completely passed
|
||||||
|
if self.scroll_position >= self.total_scroll_width:
|
||||||
|
self.scroll_position = 0
|
||||||
|
self.rotation_count += 1
|
||||||
|
|
||||||
|
# Check if we should rotate headlines
|
||||||
|
if (self.rotation_enabled and
|
||||||
|
self.rotation_count >= self.rotation_threshold and
|
||||||
|
any(len(headlines) > self.headlines_per_feed for headlines in self.news_data.values())):
|
||||||
|
self.prepare_headlines_for_display()
|
||||||
|
self.rotation_count = 0
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating news display: {e}")
|
||||||
|
return self.create_error_image(str(e))
|
||||||
|
|
||||||
|
def create_no_news_image(self) -> Image.Image:
|
||||||
|
"""Create image when no news is available"""
|
||||||
|
width = self.display_manager.width
|
||||||
|
height = self.display_manager.height
|
||||||
|
|
||||||
|
img = Image.new('RGB', (width, height), (0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
try:
|
||||||
|
font = ImageFont.truetype(self.font_path, self.font_size)
|
||||||
|
except:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
text = "Loading news..."
|
||||||
|
bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
|
text_width = bbox[2] - bbox[0]
|
||||||
|
text_height = bbox[3] - bbox[1]
|
||||||
|
|
||||||
|
x = (width - text_width) // 2
|
||||||
|
y = (height - text_height) // 2
|
||||||
|
|
||||||
|
draw.text((x, y), text, font=font, fill=self.text_color)
|
||||||
|
return img
|
||||||
|
|
||||||
|
def create_error_image(self, error_msg: str) -> Image.Image:
|
||||||
|
"""Create image for error display"""
|
||||||
|
width = self.display_manager.width
|
||||||
|
height = self.display_manager.height
|
||||||
|
|
||||||
|
img = Image.new('RGB', (width, height), (0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
try:
|
||||||
|
font = ImageFont.truetype(self.font_path, max(8, self.font_size - 2))
|
||||||
|
except:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
text = f"News Error: {error_msg[:50]}..."
|
||||||
|
bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
|
text_width = bbox[2] - bbox[0]
|
||||||
|
text_height = bbox[3] - bbox[1]
|
||||||
|
|
||||||
|
x = max(0, (width - text_width) // 2)
|
||||||
|
y = (height - text_height) // 2
|
||||||
|
|
||||||
|
draw.text((x, y), text, font=font, fill=(255, 0, 0))
|
||||||
|
return img
|
||||||
|
|
||||||
|
def display_news(self):
|
||||||
|
"""Main display method for news ticker"""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
img = self.get_news_display()
|
||||||
|
self.display_manager.display_image(img)
|
||||||
|
time.sleep(self.scroll_delay)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("News display interrupted by user")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in news display loop: {e}")
|
||||||
|
|
||||||
|
def add_custom_feed(self, name: str, url: str):
|
||||||
|
"""Add a custom RSS feed"""
|
||||||
|
if name not in self.custom_feeds:
|
||||||
|
self.custom_feeds[name] = url
|
||||||
|
# Update config
|
||||||
|
if 'news_manager' not in self.config:
|
||||||
|
self.config['news_manager'] = {}
|
||||||
|
self.config['news_manager']['custom_feeds'] = self.custom_feeds
|
||||||
|
self.config_manager.save_config(self.config)
|
||||||
|
logger.info(f"Added custom feed: {name} -> {url}")
|
||||||
|
|
||||||
|
def remove_custom_feed(self, name: str):
|
||||||
|
"""Remove a custom RSS feed"""
|
||||||
|
if name in self.custom_feeds:
|
||||||
|
del self.custom_feeds[name]
|
||||||
|
# Update config
|
||||||
|
self.config['news_manager']['custom_feeds'] = self.custom_feeds
|
||||||
|
self.config_manager.save_config(self.config)
|
||||||
|
logger.info(f"Removed custom feed: {name}")
|
||||||
|
|
||||||
|
def set_enabled_feeds(self, feeds: List[str]):
|
||||||
|
"""Set which feeds are enabled"""
|
||||||
|
self.enabled_feeds = feeds
|
||||||
|
# Update config
|
||||||
|
if 'news_manager' not in self.config:
|
||||||
|
self.config['news_manager'] = {}
|
||||||
|
self.config['news_manager']['enabled_feeds'] = self.enabled_feeds
|
||||||
|
self.config_manager.save_config(self.config)
|
||||||
|
logger.info(f"Updated enabled feeds: {self.enabled_feeds}")
|
||||||
|
|
||||||
|
# Refresh headlines
|
||||||
|
self.fetch_news_data()
|
||||||
|
|
||||||
|
def get_available_feeds(self) -> Dict[str, str]:
|
||||||
|
"""Get all available feeds (default + custom)"""
|
||||||
|
return {**self.default_feeds, **self.custom_feeds}
|
||||||
|
|
||||||
|
def get_feed_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get status information about feeds"""
|
||||||
|
status = {
|
||||||
|
'enabled_feeds': self.enabled_feeds,
|
||||||
|
'available_feeds': list(self.get_available_feeds().keys()),
|
||||||
|
'headlines_per_feed': self.headlines_per_feed,
|
||||||
|
'last_update': self.last_update,
|
||||||
|
'total_headlines': sum(len(headlines) for headlines in self.news_data.values()),
|
||||||
|
'rotation_enabled': self.rotation_enabled,
|
||||||
|
'rotation_count': self.rotation_count,
|
||||||
|
'dynamic_duration': self.dynamic_duration
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
|
||||||
|
def get_dynamic_duration(self) -> int:
|
||||||
|
"""Get the calculated dynamic duration for display"""
|
||||||
|
# Ensure we have current data and calculated duration
|
||||||
|
if not self.cached_text or self.dynamic_duration == 60:
|
||||||
|
# Try to refresh if we don't have current data
|
||||||
|
if self.should_update() or not self.current_headlines:
|
||||||
|
self.fetch_news_data()
|
||||||
|
|
||||||
|
return self.dynamic_duration
|
||||||
@@ -48,6 +48,84 @@
|
|||||||
display: none;
|
display: none;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
.checkbox-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.checkbox-item {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.checkbox-item label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.custom-feeds-container {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.add-custom-feed {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.custom-feed-item {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.custom-feed-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.remove-btn {
|
||||||
|
background-color: #ff4444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.remove-btn:hover {
|
||||||
|
background-color: #cc0000;
|
||||||
|
}
|
||||||
|
.settings-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.status-container {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f0f8f0;
|
||||||
|
}
|
||||||
|
.status-info h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #2c5e2c;
|
||||||
|
}
|
||||||
|
.status-info p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
.form-section {
|
.form-section {
|
||||||
background: #f9f9f9;
|
background: #f9f9f9;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -400,8 +478,9 @@
|
|||||||
<button class="tab-link" onclick="openTab(event, 'stocks')">Stocks & Crypto</button>
|
<button class="tab-link" onclick="openTab(event, 'stocks')">Stocks & Crypto</button>
|
||||||
<button class="tab-link" onclick="openTab(event, 'features')">Additional Features</button>
|
<button class="tab-link" onclick="openTab(event, 'features')">Additional Features</button>
|
||||||
<button class="tab-link" onclick="openTab(event, 'music')">Music</button>
|
<button class="tab-link" onclick="openTab(event, 'music')">Music</button>
|
||||||
<button class="tab-link" onclick="openTab(event, 'calendar')">Calendar</button>
|
<button class="tab-link" onclick="openTab(event, 'calendar')">Calendar</button>
|
||||||
<button class="tab-link" onclick="openTab(event, 'secrets')">API Keys</button>
|
<button class="tab-link" onclick="openTab(event, 'news')">News Manager</button>
|
||||||
|
<button class="tab-link" onclick="openTab(event, 'secrets')">API Keys</button>
|
||||||
<button class="tab-link" onclick="openTab(event, 'actions')">Actions</button>
|
<button class="tab-link" onclick="openTab(event, 'actions')">Actions</button>
|
||||||
<button class="tab-link" onclick="openTab(event, 'raw-json')">Raw JSON</button>
|
<button class="tab-link" onclick="openTab(event, 'raw-json')">Raw JSON</button>
|
||||||
<button class="tab-link" onclick="openTab(event, 'logs')">Logs</button>
|
<button class="tab-link" onclick="openTab(event, 'logs')">Logs</button>
|
||||||
@@ -2139,6 +2218,119 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- News Manager Tab -->
|
||||||
|
<div id="news" class="tab-content">
|
||||||
|
<div class="form-section">
|
||||||
|
<h2>News Manager Configuration</h2>
|
||||||
|
<p>Configure RSS news feeds and scrolling ticker settings</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="news_enabled">Enable News Manager:</label>
|
||||||
|
<div class="toggle-container">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="news_enabled" name="news_enabled">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="headlines_per_feed">Headlines Per Feed:</label>
|
||||||
|
<input type="number" id="headlines_per_feed" name="headlines_per_feed" min="1" max="5" value="2">
|
||||||
|
<div class="description">Number of headlines to show from each enabled feed</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Available News Feeds:</label>
|
||||||
|
<div class="checkbox-grid" id="news_feeds_grid">
|
||||||
|
<!-- Feeds will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>Custom RSS Feeds</h3>
|
||||||
|
<div class="custom-feeds-container">
|
||||||
|
<div class="add-custom-feed">
|
||||||
|
<input type="text" id="custom_feed_name" placeholder="Feed Name" style="width: 200px;">
|
||||||
|
<input type="text" id="custom_feed_url" placeholder="RSS Feed URL" style="width: 400px;">
|
||||||
|
<button type="button" onclick="addCustomFeed()">Add Feed</button>
|
||||||
|
</div>
|
||||||
|
<div id="custom_feeds_list">
|
||||||
|
<!-- Custom feeds will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>Scrolling Settings</h3>
|
||||||
|
<div class="settings-row">
|
||||||
|
<div>
|
||||||
|
<label for="scroll_speed">Scroll Speed:</label>
|
||||||
|
<input type="number" id="scroll_speed" min="1" max="10" value="2">
|
||||||
|
<div class="description">Pixels per frame</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="scroll_delay">Scroll Delay (ms):</label>
|
||||||
|
<input type="number" id="scroll_delay" min="5" max="100" value="20">
|
||||||
|
<div class="description">Delay between scroll updates</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dynamic_duration">Enable Dynamic Duration:</label>
|
||||||
|
<div class="toggle-container">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="dynamic_duration" name="dynamic_duration" checked>
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="description">Automatically calculate display time based on headline length</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>Duration Settings</h3>
|
||||||
|
<div class="settings-row">
|
||||||
|
<div>
|
||||||
|
<label for="min_duration">Min Duration (s):</label>
|
||||||
|
<input type="number" id="min_duration" min="10" max="120" value="30">
|
||||||
|
<div class="description">Minimum display time</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="max_duration">Max Duration (s):</label>
|
||||||
|
<input type="number" id="max_duration" min="60" max="600" value="300">
|
||||||
|
<div class="description">Maximum display time</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="duration_buffer">Buffer (%):</label>
|
||||||
|
<input type="number" id="duration_buffer" min="0" max="50" value="10" step="5">
|
||||||
|
<div class="description">Extra time for smooth cycling</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="rotation_enabled">Enable Headline Rotation:</label>
|
||||||
|
<div class="toggle-container">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="rotation_enabled" name="rotation_enabled" checked>
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="description">Rotate through different headlines to avoid repetition</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button type="button" onclick="saveNewsSettings()">Save News Settings</button>
|
||||||
|
<button type="button" onclick="refreshNewsStatus()">Refresh Status</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="news_status" class="status-container" style="margin-top: 20px;">
|
||||||
|
<!-- Status will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Secrets Tab -->
|
<!-- Secrets Tab -->
|
||||||
<div id="secrets" class="tab-content">
|
<div id="secrets" class="tab-content">
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
@@ -3533,6 +3725,214 @@
|
|||||||
logContent.textContent = `Error loading logs: ${error}`;
|
logContent.textContent = `Error loading logs: ${error}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// News Manager Functions
|
||||||
|
let newsManagerData = {};
|
||||||
|
|
||||||
|
function loadNewsManagerData() {
|
||||||
|
fetch('/news_manager/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
newsManagerData = data.data;
|
||||||
|
updateNewsManagerUI();
|
||||||
|
} else {
|
||||||
|
console.error('Error loading news manager data:', data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading news manager data:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNewsManagerUI() {
|
||||||
|
// Update enabled toggle
|
||||||
|
document.getElementById('news_enabled').checked = newsManagerData.enabled || false;
|
||||||
|
|
||||||
|
// Update headlines per feed
|
||||||
|
document.getElementById('headlines_per_feed').value = newsManagerData.headlines_per_feed || 2;
|
||||||
|
|
||||||
|
// Update rotation enabled
|
||||||
|
document.getElementById('rotation_enabled').checked = newsManagerData.rotation_enabled !== false;
|
||||||
|
|
||||||
|
// Populate available feeds
|
||||||
|
const feedsGrid = document.getElementById('news_feeds_grid');
|
||||||
|
feedsGrid.innerHTML = '';
|
||||||
|
|
||||||
|
if (newsManagerData.available_feeds) {
|
||||||
|
newsManagerData.available_feeds.forEach(feed => {
|
||||||
|
const isEnabled = newsManagerData.enabled_feeds.includes(feed);
|
||||||
|
const feedDiv = document.createElement('div');
|
||||||
|
feedDiv.className = 'checkbox-item';
|
||||||
|
feedDiv.innerHTML = `
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="news_feed" value="${feed}" ${isEnabled ? 'checked' : ''}>
|
||||||
|
${feed}
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
feedsGrid.appendChild(feedDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate custom feeds
|
||||||
|
updateCustomFeedsList();
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
updateNewsStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCustomFeedsList() {
|
||||||
|
const customFeedsList = document.getElementById('custom_feeds_list');
|
||||||
|
customFeedsList.innerHTML = '';
|
||||||
|
|
||||||
|
if (newsManagerData.custom_feeds) {
|
||||||
|
Object.entries(newsManagerData.custom_feeds).forEach(([name, url]) => {
|
||||||
|
const feedDiv = document.createElement('div');
|
||||||
|
feedDiv.className = 'custom-feed-item';
|
||||||
|
feedDiv.innerHTML = `
|
||||||
|
<div class="custom-feed-info">
|
||||||
|
<strong>${name}</strong>: ${url}
|
||||||
|
<button type="button" onclick="removeCustomFeed('${name}')" class="remove-btn">Remove</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
customFeedsList.appendChild(feedDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNewsStatus() {
|
||||||
|
const statusDiv = document.getElementById('news_status');
|
||||||
|
const enabledFeeds = newsManagerData.enabled_feeds || [];
|
||||||
|
const totalFeeds = enabledFeeds.length + Object.keys(newsManagerData.custom_feeds || {}).length;
|
||||||
|
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="status-info">
|
||||||
|
<h4>Current Status</h4>
|
||||||
|
<p><strong>Enabled:</strong> ${newsManagerData.enabled ? 'Yes' : 'No'}</p>
|
||||||
|
<p><strong>Active Feeds:</strong> ${enabledFeeds.join(', ') || 'None'}</p>
|
||||||
|
<p><strong>Headlines per Feed:</strong> ${newsManagerData.headlines_per_feed || 2}</p>
|
||||||
|
<p><strong>Total Custom Feeds:</strong> ${Object.keys(newsManagerData.custom_feeds || {}).length}</p>
|
||||||
|
<p><strong>Rotation Enabled:</strong> ${newsManagerData.rotation_enabled !== false ? 'Yes' : 'No'}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveNewsSettings() {
|
||||||
|
// Get enabled feeds
|
||||||
|
const enabledFeeds = Array.from(document.querySelectorAll('input[name="news_feed"]:checked'))
|
||||||
|
.map(input => input.value);
|
||||||
|
|
||||||
|
const headlinesPerFeed = parseInt(document.getElementById('headlines_per_feed').value);
|
||||||
|
const enabled = document.getElementById('news_enabled').checked;
|
||||||
|
const rotationEnabled = document.getElementById('rotation_enabled').checked;
|
||||||
|
|
||||||
|
// Save enabled status first
|
||||||
|
fetch('/news_manager/toggle', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ enabled: enabled })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Then save feed settings
|
||||||
|
return fetch('/news_manager/update_feeds', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
enabled_feeds: enabledFeeds,
|
||||||
|
headlines_per_feed: headlinesPerFeed
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showMessage('News settings saved successfully!', 'success');
|
||||||
|
loadNewsManagerData(); // Refresh the data
|
||||||
|
} else {
|
||||||
|
showMessage('Error saving news settings: ' + data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showMessage('Error saving news settings: ' + error, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCustomFeed() {
|
||||||
|
const name = document.getElementById('custom_feed_name').value.trim();
|
||||||
|
const url = document.getElementById('custom_feed_url').value.trim();
|
||||||
|
|
||||||
|
if (!name || !url) {
|
||||||
|
showMessage('Please enter both feed name and URL', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/news_manager/add_custom_feed', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: name, url: url })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showMessage(data.message, 'success');
|
||||||
|
document.getElementById('custom_feed_name').value = '';
|
||||||
|
document.getElementById('custom_feed_url').value = '';
|
||||||
|
loadNewsManagerData(); // Refresh the data
|
||||||
|
} else {
|
||||||
|
showMessage('Error adding custom feed: ' + data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showMessage('Error adding custom feed: ' + error, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCustomFeed(name) {
|
||||||
|
if (!confirm(`Are you sure you want to remove the feed "${name}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/news_manager/remove_custom_feed', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: name })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showMessage(data.message, 'success');
|
||||||
|
loadNewsManagerData(); // Refresh the data
|
||||||
|
} else {
|
||||||
|
showMessage('Error removing custom feed: ' + data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showMessage('Error removing custom feed: ' + error, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshNewsStatus() {
|
||||||
|
loadNewsManagerData();
|
||||||
|
showMessage('News status refreshed', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load news manager data when the news tab is opened
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Override the openTab function to load news data when news tab is opened
|
||||||
|
const originalOpenTab = window.openTab;
|
||||||
|
window.openTab = function(evt, tabName) {
|
||||||
|
originalOpenTab(evt, tabName);
|
||||||
|
if (tabName === 'news') {
|
||||||
|
loadNewsManagerData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
154
web_interface.py
154
web_interface.py
@@ -351,5 +351,159 @@ def save_raw_json_route():
|
|||||||
'message': f'Error saving raw JSON: {str(e)}'
|
'message': f'Error saving raw JSON: {str(e)}'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
|
@app.route('/news_manager/status', methods=['GET'])
|
||||||
|
def get_news_manager_status():
|
||||||
|
"""Get news manager status and configuration"""
|
||||||
|
try:
|
||||||
|
config = config_manager.load_config()
|
||||||
|
news_config = config.get('news_manager', {})
|
||||||
|
|
||||||
|
# Try to get status from the running display controller if possible
|
||||||
|
status = {
|
||||||
|
'enabled': news_config.get('enabled', False),
|
||||||
|
'enabled_feeds': news_config.get('enabled_feeds', []),
|
||||||
|
'available_feeds': [
|
||||||
|
'MLB', 'NFL', 'NCAA FB', 'NHL', 'NBA', 'TOP SPORTS',
|
||||||
|
'BIG10', 'NCAA', 'Other'
|
||||||
|
],
|
||||||
|
'headlines_per_feed': news_config.get('headlines_per_feed', 2),
|
||||||
|
'rotation_enabled': news_config.get('rotation_enabled', True),
|
||||||
|
'custom_feeds': news_config.get('custom_feeds', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'data': status
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Error getting news manager status: {str(e)}'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
@app.route('/news_manager/update_feeds', methods=['POST'])
|
||||||
|
def update_news_feeds():
|
||||||
|
"""Update enabled news feeds"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
enabled_feeds = data.get('enabled_feeds', [])
|
||||||
|
headlines_per_feed = data.get('headlines_per_feed', 2)
|
||||||
|
|
||||||
|
config = config_manager.load_config()
|
||||||
|
if 'news_manager' not in config:
|
||||||
|
config['news_manager'] = {}
|
||||||
|
|
||||||
|
config['news_manager']['enabled_feeds'] = enabled_feeds
|
||||||
|
config['news_manager']['headlines_per_feed'] = headlines_per_feed
|
||||||
|
|
||||||
|
config_manager.save_config(config)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'News feeds updated successfully!'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Error updating news feeds: {str(e)}'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
@app.route('/news_manager/add_custom_feed', methods=['POST'])
|
||||||
|
def add_custom_news_feed():
|
||||||
|
"""Add a custom RSS feed"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
url = data.get('url', '').strip()
|
||||||
|
|
||||||
|
if not name or not url:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Name and URL are required'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
config = config_manager.load_config()
|
||||||
|
if 'news_manager' not in config:
|
||||||
|
config['news_manager'] = {}
|
||||||
|
if 'custom_feeds' not in config['news_manager']:
|
||||||
|
config['news_manager']['custom_feeds'] = {}
|
||||||
|
|
||||||
|
config['news_manager']['custom_feeds'][name] = url
|
||||||
|
config_manager.save_config(config)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': f'Custom feed "{name}" added successfully!'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Error adding custom feed: {str(e)}'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
@app.route('/news_manager/remove_custom_feed', methods=['POST'])
|
||||||
|
def remove_custom_news_feed():
|
||||||
|
"""Remove a custom RSS feed"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Feed name is required'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
config = config_manager.load_config()
|
||||||
|
custom_feeds = config.get('news_manager', {}).get('custom_feeds', {})
|
||||||
|
|
||||||
|
if name in custom_feeds:
|
||||||
|
del custom_feeds[name]
|
||||||
|
config_manager.save_config(config)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': f'Custom feed "{name}" removed successfully!'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Custom feed "{name}" not found'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Error removing custom feed: {str(e)}'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
@app.route('/news_manager/toggle', methods=['POST'])
|
||||||
|
def toggle_news_manager():
|
||||||
|
"""Toggle news manager on/off"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
enabled = data.get('enabled', False)
|
||||||
|
|
||||||
|
config = config_manager.load_config()
|
||||||
|
if 'news_manager' not in config:
|
||||||
|
config['news_manager'] = {}
|
||||||
|
|
||||||
|
config['news_manager']['enabled'] = enabled
|
||||||
|
config_manager.save_config(config)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': f'News manager {"enabled" if enabled else "disabled"} successfully!'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Error toggling news manager: {str(e)}'
|
||||||
|
}), 400
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
Reference in New Issue
Block a user