Skip to content

Commit 711482d

Browse files
authored
Add news source logos (#143)
* Download favicons; Display first one * Refine image loading * Switch to static images * Remove unused var * Fix * Clean up * Fix width fallback
1 parent 694c7ce commit 711482d

File tree

5 files changed

+102
-68
lines changed

5 files changed

+102
-68
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ token.pickle
1515
.venv
1616
env/
1717
venv/
18+
venv*/
1819
ENV/
1920

2021
# IDE

assets/news_logos/cbc.png

39.5 KB
Loading

assets/news_logos/cnn.png

56.3 KB
Loading

src/image_utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import logging
2+
from PIL import Image
3+
4+
logger = logging.getLogger(__name__)
5+
6+
def scale_to_max_dimensions(img, max_width, max_height):
7+
h_to_w_ratio = img.height / img.width
8+
w_to_h_ratio = img.width / img.height
9+
10+
if img.height > max_height:
11+
img = img.resize((int(max_height * w_to_h_ratio), max_height), Image.Resampling.LANCZOS)
12+
13+
if img.width > max_width:
14+
img = img.resize((max_width, int(max_width * h_to_w_ratio)), Image.Resampling.LANCZOS)
15+
16+
return img

src/news_manager.py

Lines changed: 85 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import time
1+
import html
22
import logging
3-
import requests
4-
import xml.etree.ElementTree as ET
5-
import json
6-
import random
7-
from typing import Dict, Any, List, Tuple, Optional
8-
from datetime import datetime, timedelta
93
import os
10-
import urllib.parse
114
import re
12-
import html
5+
import requests
6+
import time
7+
import xml.etree.ElementTree as ET
8+
from typing import Dict, Any, List
9+
from datetime import datetime
10+
from src.image_utils import scale_to_max_dimensions
1311
from src.config_manager import ConfigManager
1412
from PIL import Image, ImageDraw, ImageFont
1513
from src.cache_manager import CacheManager
@@ -37,10 +35,11 @@ def __init__(self, config: Dict[str, Any], display_manager, config_manager=None)
3735
self.news_config = config.get('news_manager', {})
3836
self.last_update = time.time() # Initialize to current time
3937
self.news_data = {}
38+
self.favicons = {}
4039
self.current_headline_index = 0
4140
self.scroll_position = 0
4241
self.scrolling_image = None # Pre-rendered image for smooth scrolling
43-
self.cached_text = None
42+
self.cached_images = []
4443
self.cache_manager = CacheManager()
4544
self.current_headlines = []
4645
self.headline_start_times = []
@@ -101,6 +100,13 @@ def __init__(self, config: Dict[str, Any], display_manager, config_manager=None)
101100
adapter = HTTPAdapter(max_retries=retry_strategy)
102101
self.session.mount("http://", adapter)
103102
self.session.mount("https://", adapter)
103+
104+
try:
105+
self.font = ImageFont.truetype(self.font_path, self.font_size)
106+
logger.debug(f"Successfully loaded custom font: {self.font_path}")
107+
except Exception as e:
108+
logger.warning(f"Failed to load custom font '{self.font_path}': {e}. Using default font.")
109+
self.font = ImageFont.load_default()
104110

105111
logger.debug(f"NewsManager initialized with feeds: {self.enabled_feeds}")
106112
logger.debug(f"Headlines per feed: {self.headlines_per_feed}")
@@ -112,7 +118,6 @@ def parse_rss_feed(self, url: str, feed_name: str) -> List[Dict[str, Any]]:
112118
headers = {
113119
'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'
114120
}
115-
116121
response = self.session.get(url, headers=headers, timeout=10)
117122
response.raise_for_status()
118123

@@ -156,6 +161,16 @@ def parse_rss_feed(self, url: str, feed_name: str) -> List[Dict[str, Any]]:
156161
except Exception as e:
157162
logger.error(f"Error parsing RSS feed {feed_name} ({url}): {e}")
158163
return []
164+
165+
def load_favicon(self, feed_name):
166+
try:
167+
img_path = os.path.join('assets', 'news_logos', f"{feed_name.lower()}.png")
168+
with Image.open(img_path) as img:
169+
img = scale_to_max_dimensions(img, 32, int(self.display_manager.height * 0.8)).convert('RGBA')
170+
self.favicons[feed_name] = img.copy()
171+
except Exception as e:
172+
logger.error(f"Error loading favicon for {feed_name}: {e}")
173+
return
159174

160175
def fetch_news_data(self):
161176
"""Fetch news from all enabled feeds"""
@@ -170,6 +185,7 @@ def fetch_news_data(self):
170185
url = all_feeds[feed_name]
171186
headlines = self.parse_rss_feed(url, feed_name)
172187
all_headlines.extend(headlines)
188+
self.load_favicon(feed_name)
173189
else:
174190
logger.warning(f"Feed '{feed_name}' not found in available feeds")
175191

@@ -215,81 +231,96 @@ def prepare_headlines_for_display(self):
215231

216232
# Create scrolling text with separators
217233
if display_headlines:
218-
text_parts = []
234+
self.cached_images = []
219235
for i, headline in enumerate(display_headlines):
220-
feed_prefix = f"[{headline['feed']}] "
221-
text_parts.append(feed_prefix + headline['title'])
236+
favicon = self.favicons.get(headline['feed'])
237+
238+
# Use backup separator and prefix if no logo for feed
239+
separator = " • " if not favicon and i > 0 else ''
240+
feed_prefix = f"[{headline['feed']}] " if not favicon else ''
241+
text = separator + feed_prefix + headline['title']
242+
243+
# Calculate text width and X value
244+
text_width = self._get_text_width(text, self.font)
245+
headline_width = text_width
246+
text_x_pos = 0
247+
if favicon:
248+
text_x_pos = favicon.width + 16
249+
headline_width += text_x_pos
250+
251+
# Draw Image
252+
img = Image.new('RGB', (headline_width, self.display_manager.height), (0, 0, 0))
253+
draw = ImageDraw.Draw(img)
254+
if favicon:
255+
logo_x = 10
256+
logo_y = (self.display_manager.height - favicon.height) // 2
257+
img.paste(favicon, (logo_x, logo_y), favicon)
222258

223-
# Join with separators and add spacing
224-
separator = " • "
225-
self.cached_text = separator.join(text_parts) + " • " # Add separator at end for smooth loop
259+
# Draw text
260+
text_height = self.font_size
261+
y_pos = (self.display_manager.height - text_height) // 2
262+
draw.text((text_x_pos, y_pos), text, font=self.font, fill=self.text_color)
263+
264+
# Append to cached images for rendering in `create_scrolling_image()`
265+
self.cached_images.append(img)
226266

267+
self.current_headlines = display_headlines
268+
227269
# Calculate text dimensions for perfect scrolling
228270
self.calculate_scroll_dimensions()
229271
self.create_scrolling_image()
230272

231-
self.current_headlines = display_headlines
232273
logger.debug(f"Prepared {len(display_headlines)} headlines for display")
233274

234275
def calculate_scroll_dimensions(self):
235276
"""Calculate exact dimensions needed for smooth scrolling"""
236-
if not self.cached_text:
277+
if not self.cached_images:
237278
return
238279

239280
try:
240-
# Load font
241-
try:
242-
font = ImageFont.truetype(self.font_path, self.font_size)
243-
logger.debug(f"Successfully loaded custom font: {self.font_path}")
244-
except Exception as e:
245-
logger.warning(f"Failed to load custom font '{self.font_path}': {e}. Using default font.")
246-
font = ImageFont.load_default()
247-
248-
# Calculate text width
249-
temp_img = Image.new('RGB', (1, 1))
250-
temp_draw = ImageDraw.Draw(temp_img)
251-
252-
# Get text dimensions
253-
bbox = temp_draw.textbbox((0, 0), self.cached_text, font=font)
254-
text_width = bbox[2] - bbox[0]
255-
# Add display width gap at the beginning (simulates blank screen)
256281
display_width = self.display_manager.width
257-
self.total_scroll_width = display_width + text_width
282+
self.total_scroll_width = display_width
283+
for img in self.cached_images:
284+
self.total_scroll_width += img.width
258285

259286
# Calculate dynamic display duration
260287
self.calculate_dynamic_duration()
261288

262-
logger.debug(f"Text width calculated: {self.total_scroll_width} pixels")
289+
logger.debug(f"Image width calculated: {self.total_scroll_width} pixels")
263290
logger.debug(f"Dynamic duration calculated: {self.dynamic_duration} seconds")
264291

265292
except Exception as e:
266293
logger.error(f"Error calculating scroll dimensions: {e}")
267-
self.total_scroll_width = len(self.cached_text) * 8 # Fallback estimate
294+
self.total_scroll_width = sum(len(x['title']) for x in self.current_headlines) * 8 # Fallback estimate
268295
self.calculate_dynamic_duration()
269296

297+
def _get_text_width(self, text, font):
298+
temp_img = Image.new('RGB', (1, 1))
299+
temp_draw = ImageDraw.Draw(temp_img)
300+
301+
# Get text dimensions
302+
bbox = temp_draw.textbbox((0, 0), text, font=font)
303+
return bbox[2] - bbox[0]
304+
305+
270306
def create_scrolling_image(self):
271307
"""Create a pre-rendered image for smooth scrolling."""
272-
if not self.cached_text:
308+
if not self.cached_images:
273309
self.scrolling_image = None
274310
return
275311

276-
try:
277-
font = ImageFont.truetype(self.font_path, self.font_size)
278-
except Exception as e:
279-
logger.warning(f"Failed to load custom font for pre-rendering: {e}. Using default.")
280-
font = ImageFont.load_default()
281-
282312
height = self.display_manager.height
283313
width = self.total_scroll_width
284314

285315
self.scrolling_image = Image.new('RGB', (width, height), (0, 0, 0))
286-
draw = ImageDraw.Draw(self.scrolling_image)
287316

288-
text_height = self.font_size
289-
y_pos = (height - text_height) // 2
290317
# Draw text starting after display width gap (simulates blank screen)
291-
display_width = self.display_manager.width
292-
draw.text((display_width, y_pos), self.cached_text, font=font, fill=self.text_color)
318+
x_pos = self.display_manager.width
319+
for img in self.cached_images:
320+
# Render each cached image and advance the cursor by the width of the image
321+
self.scrolling_image.paste(img, (x_pos, 0))
322+
x_pos += img.width
323+
293324
logger.debug("Pre-rendered scrolling news image created.")
294325

295326
def calculate_dynamic_duration(self):
@@ -405,22 +436,15 @@ def create_no_news_image(self) -> Image.Image:
405436
img = Image.new('RGB', (width, height), (0, 0, 0))
406437
draw = ImageDraw.Draw(img)
407438

408-
try:
409-
font = ImageFont.truetype(self.font_path, self.font_size)
410-
logger.debug(f"Successfully loaded custom font: {self.font_path}")
411-
except Exception as e:
412-
logger.warning(f"Failed to load custom font '{self.font_path}': {e}. Using default font.")
413-
font = ImageFont.load_default()
414-
415439
text = "Loading news..."
416-
bbox = draw.textbbox((0, 0), text, font=font)
440+
bbox = draw.textbbox((0, 0), text, font=self.font)
417441
text_width = bbox[2] - bbox[0]
418442
text_height = bbox[3] - bbox[1]
419443

420444
x = (width - text_width) // 2
421445
y = (height - text_height) // 2
422446

423-
draw.text((x, y), text, font=font, fill=self.text_color)
447+
draw.text((x, y), text, font=self.font, fill=self.text_color)
424448
return img
425449

426450
def create_error_image(self, error_msg: str) -> Image.Image:
@@ -431,22 +455,15 @@ def create_error_image(self, error_msg: str) -> Image.Image:
431455
img = Image.new('RGB', (width, height), (0, 0, 0))
432456
draw = ImageDraw.Draw(img)
433457

434-
try:
435-
font = ImageFont.truetype(self.font_path, max(8, self.font_size - 2))
436-
logger.debug(f"Successfully loaded custom font: {self.font_path}")
437-
except Exception as e:
438-
logger.warning(f"Failed to load custom font '{self.font_path}': {e}. Using default font.")
439-
font = ImageFont.load_default()
440-
441458
text = f"News Error: {error_msg[:50]}..."
442-
bbox = draw.textbbox((0, 0), text, font=font)
459+
bbox = draw.textbbox((0, 0), text, font=self.font)
443460
text_width = bbox[2] - bbox[0]
444461
text_height = bbox[3] - bbox[1]
445462

446463
x = max(0, (width - text_width) // 2)
447464
y = (height - text_height) // 2
448465

449-
draw.text((x, y), text, font=font, fill=(255, 0, 0))
466+
draw.text((x, y), text, font=self.font, fill=(255, 0, 0))
450467
return img
451468

452469
def display_news(self, force_clear: bool = False):

0 commit comments

Comments
 (0)