This commit is contained in:
death916 2025-11-05 04:28:37 -08:00
parent 0b12a928f5
commit 2b09d76131
4 changed files with 138 additions and 66 deletions

View file

@ -11,12 +11,12 @@ from typing import Any, Dict, List
import reflex as rx import reflex as rx
from utils.news import News from utils.news import News
from utils.scores import NBAScores, mlbScores, nflScores from utils.scores import NBAScores, mlbScores, nflScores
# --- Import your Weather utility ---
from utils.weather import Weather from utils.weather import Weather
# --- Constants --- # --- Constants ---
WEATHER_IMAGE_PATH = "/weather.jpg" # Web path in assets folder WEATHER_IMAGE_PATH = "/weather.jpg" # Web path in assets folder
WEATHER_FETCH_INTERVAL = 360 WEATHER_FETCH_INTERVAL = 360
@ -28,12 +28,13 @@ logging.basicConfig(
class State(rx.State): class State(rx.State):
# --- State Variables --- # --- State Variables ---
current_time: str = ( current_time: str = ""
"" # Note: rx.moment replaces the need for this if used for display
)
alarm_time: str = "" alarm_time: str = ""
alarms: list = [] alarms: List = []
news: List[Dict[str, Any]] = [] news: List[Dict[str, Any]] = []
current_news_index: int = 0
news_rotate_interval: int = 10
news_text: str = ""
nba_scores: List[Dict[str, Any]] = [] nba_scores: List[Dict[str, Any]] = []
mlb_scores: List[Dict[str, Any]] = [] mlb_scores: List[Dict[str, Any]] = []
nfl_scores: List[Dict[str, Any]] = [] nfl_scores: List[Dict[str, Any]] = []
@ -75,7 +76,12 @@ class State(rx.State):
) # trying to test themes remove after ) # trying to test themes remove after
logging.info("Triggering background tasks: Weather") logging.info("Triggering background tasks: Weather")
# Return a list containing the handler references # Return a list containing the handler references
return [State.fetch_weather, State.fetch_sports] return [
State.fetch_weather,
State.fetch_sports,
State.fetch_news,
State.cycle_news,
]
# --- Sports Background Task --- # --- Sports Background Task ---
@ -140,31 +146,46 @@ class State(rx.State):
) )
await asyncio.sleep(500) await asyncio.sleep(500)
"""@rx.event(background=True) @rx.event(background=True)
async def fetch_news(self): async def fetch_news(self):
# Fetches news periodically while True:
# Placeholder for the actual news fetching logic try:
while True: logging.info("Fetching news...")
try: news_items = await self._news_client.get_news()
logging.info("Fetching news...") if not news_items:
news_items = await self._news_client.get_news() logging.warning("No news items fetched.")
if not news_items: async with self:
logging.warning("No news items fetched.") self.news = []
async with self: self.current_news_index = 0
self.news = [] yield
yield # Update frontend else:
else: logging.info(f"Fetched {len(news_items)} news items.")
logging.info(f"Fetched {len(news_items)} news items.") async with self:
async with self: self.news = news_items
self.news = news_items self.current_news_index = 0
yield yield
except Exception as e: except Exception as e:
logging.error( logging.error(
f"Error in fetch_news background task: {e}", exc_info=True f"Error in fetch_news background task: {e}", exc_info=True
) )
await asyncio.sleep(500) await asyncio.sleep(500)
"""
@rx.event(background=True)
async def cycle_news(self):
while True:
try:
if self.news:
async with self:
self.current_news_index = (self.current_news_index + 1) % len(
self.news
)
yield
except Exception as e:
logging.error(
f"Error in cycle_news background task: {e}", exc_info=True
)
await asyncio.sleep(self.news_rotate_interval)
# --- Weather Background Task --- # --- Weather Background Task ---
@rx.event(background=True) @rx.event(background=True)
@ -350,6 +371,36 @@ def index() -> rx.Component:
background_color="#6f42c1", background_color="#6f42c1",
) )
news_card = rx.card(
rx.box(
rx.text("News Headlines"),
rx.center(
rx.cond(
State.news,
rx.text(
State.news[State.current_news_index]["title"],
font_size="lg",
color="gray.500",
no_of_lines=1,
white_space="nowrap",
overflow="hidden",
text_overflow="ellipsis",
),
rx.text(
"No news available",
font_size="lg",
color="gray.500",
),
),
width="100%",
min_height="3.25rem",
),
padding="4",
),
variant="surface",
width="100%",
)
# Compose the page # Compose the page
page = rx.container( # pyright: ignore[reportReturnType] page = rx.container( # pyright: ignore[reportReturnType]
rx.theme_panel(default_open=False), rx.theme_panel(default_open=False),
@ -357,6 +408,7 @@ def index() -> rx.Component:
rx.vstack( rx.vstack(
clock_button, clock_button,
main_flex, main_flex,
news_card,
align="center", align="center",
spacing="4", spacing="4",
) )

View file

@ -15,6 +15,7 @@ dependencies = [
"mlb-statsapi>=1.8.1", "mlb-statsapi>=1.8.1",
"reflex>=0.6.8", "reflex>=0.6.8",
"aiohttp>=3.11.18", "aiohttp>=3.11.18",
"reflex-text-loop>=0.0.1",
] ]
[dependency-groups] [dependency-groups]

View file

@ -6,19 +6,19 @@ from time import localtime, strftime
import socket import socket
import aiohttp import aiohttp
def print_time(): def print_time():
print(strftime("%B %d, %I:%M %p", localtime())) print(strftime("%B %d, %I:%M %p", localtime()))
class News: class News:
def __init__(self): def __init__(self):
self._news_dict = {} socket.setdefaulttimeout(10) # Set default timeout for socket operations
self._news_dict_length = 0
socket.setdefaulttimeout(10) # Set default timeout for socket operations
async def _fetch_feed(self, session, feed): async def _fetch_feed(self, session, feed):
"""Fetches and parses a single feed asynchronously.""" """Fetches and parses a single feed asynchronously."""
max_entries = 10 # Maximum number of entries to fetch from each feed max_entries = 10 # Maximum number of entries to fetch from each feed
try: try:
# Add timeout to the request # Add timeout to the request
timeout = aiohttp.ClientTimeout(total=5) timeout = aiohttp.ClientTimeout(total=5)
@ -26,30 +26,36 @@ class News:
if response.status != 200: if response.status != 200:
print(f"Skip feed {feed}: status {response.status}") print(f"Skip feed {feed}: status {response.status}")
return [] return []
text = await response.text() text = await response.text()
d = feedparser.parse(text) d = feedparser.parse(text)
if hasattr(d, 'status') and d.status != 200: if hasattr(d, "status") and d.status != 200:
print(f"Skip feed {feed}: status {d.status}") print(f"Skip feed {feed}: status {d.status}")
return [] return []
feed_entries = [] feed_entries = []
# Limit the number of entries parsed # Limit the number of entries parsed
for i, post in enumerate(d.entries): for i, post in enumerate(d.entries):
if i >= max_entries: if i >= max_entries:
break # Stop parsing if we've reached the limit break # Stop parsing if we've reached the limit
feed_entries.append({ feed_entries.append(
'title': post.title, {
'source': d.feed.title if hasattr(d.feed, 'title') else 'Unknown', "title": post.title,
'publish_date': post.published if hasattr(post, 'published') else '', "source": d.feed.title
'summary': post.summary if hasattr(post, 'summary') else '' if hasattr(d.feed, "title")
}) else "Unknown",
"publish_date": post.published
if hasattr(post, "published")
else "",
"summary": post.summary if hasattr(post, "summary") else "",
}
)
print(f"Added {len(feed_entries)} entries from {feed}") print(f"Added {len(feed_entries)} entries from {feed}")
return feed_entries return feed_entries
except aiohttp.ClientError as e: except aiohttp.ClientError as e:
print(f"Error processing feed {feed}: {e}") print(f"Error processing feed {feed}: {e}")
return [] return []
@ -62,7 +68,7 @@ class News:
feeds = [] feeds = []
self._news_dict = {} self._news_dict = {}
self._news_dict_length = 0 self._news_dict_length = 0
try: try:
async with aiofiles.open("feeds.txt", "r") as f: async with aiofiles.open("feeds.txt", "r") as f:
async for line in f: async for line in f:
@ -70,38 +76,37 @@ class News:
except Exception as e: except Exception as e:
print(f"Error reading feeds.txt: {e}") print(f"Error reading feeds.txt: {e}")
return {} return {}
# Limit the number of feeds to process at once # Limit the number of feeds to process at once
if len(feeds) > 10: if len(feeds) > 10:
feeds = random.sample(feeds, 10) feeds = random.sample(feeds, 10)
print("Getting news entries...") print("Getting news entries...")
timeout = aiohttp.ClientTimeout(total=15) timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
tasks = [self._fetch_feed(session, feed) for feed in feeds] tasks = [self._fetch_feed(session, feed) for feed in feeds]
all_feed_entries_list = await asyncio.gather(*tasks, return_exceptions=True) all_feed_entries_list = await asyncio.gather(*tasks, return_exceptions=True)
all_entries = [] all_entries = []
for result in all_feed_entries_list: for result in all_feed_entries_list:
if isinstance(result, list) and result: if isinstance(result, list) and result:
all_entries.extend(result) all_entries.extend(result)
if not all_entries: if not all_entries:
print("No entries collected") print("No entries collected")
return {} return []
if len(all_entries) > 30: if len(all_entries) > 30:
all_entries = random.sample(all_entries, 30) all_entries = random.sample(all_entries, 30)
for entry in all_entries:
self._news_dict[entry['title']] = entry
try: try:
async with aiofiles.open("news.txt", "w") as f: async with aiofiles.open("news.txt", "w") as f:
print("Writing news to file...") print("Writing news to file...")
for entry in self._news_dict.values(): for entry in all_entries:
await f.write(f"[{entry['publish_date']}] {entry['source']}: {entry['title']}\n") await f.write(
f"[{entry['publish_date']}] {entry['source']}: {entry['title']}\n"
)
except Exception as e: except Exception as e:
print(f"Error writing to news.txt: {e}") print(f"Error writing to news.txt: {e}")
return self._news_dict return all_entries

14
uv.lock generated
View file

@ -268,6 +268,7 @@ dependencies = [
{ name = "nba-api" }, { name = "nba-api" },
{ name = "playwright" }, { name = "playwright" },
{ name = "reflex" }, { name = "reflex" },
{ name = "reflex-text-loop" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" }, { name = "rich" },
] ]
@ -287,6 +288,7 @@ requires-dist = [
{ name = "nba-api", specifier = ">=1.6.1,<2" }, { name = "nba-api", specifier = ">=1.6.1,<2" },
{ name = "playwright", specifier = ">=1.49.1,<2" }, { name = "playwright", specifier = ">=1.49.1,<2" },
{ name = "reflex", specifier = ">=0.6.8" }, { name = "reflex", specifier = ">=0.6.8" },
{ name = "reflex-text-loop", specifier = ">=0.0.1" },
{ name = "requests", specifier = ">=2.31.0,<3" }, { name = "requests", specifier = ">=2.31.0,<3" },
{ name = "rich", specifier = ">=13.7.1,<14" }, { name = "rich", specifier = ">=13.7.1,<14" },
] ]
@ -1099,6 +1101,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/4d/5526f5a21a01b6179e3ebe9abc46dce01d9692ef655e22002a08e10de8b7/reflex_hosting_cli-0.1.42-py3-none-any.whl", hash = "sha256:2742d9eb3576fa001ea2a4971cef1e1db56a08aad619aa035af3b6cc8f80de4c", size = 38668, upload-time = "2025-04-11T17:28:02.296Z" }, { url = "https://files.pythonhosted.org/packages/0a/4d/5526f5a21a01b6179e3ebe9abc46dce01d9692ef655e22002a08e10de8b7/reflex_hosting_cli-0.1.42-py3-none-any.whl", hash = "sha256:2742d9eb3576fa001ea2a4971cef1e1db56a08aad619aa035af3b6cc8f80de4c", size = 38668, upload-time = "2025-04-11T17:28:02.296Z" },
] ]
[[package]]
name = "reflex-text-loop"
version = "0.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "reflex" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/eb/2bf65bab5474452b374951497f82338abc95569a5451dfade2ba52e05133/reflex_text_loop-0.0.1.tar.gz", hash = "sha256:4be9cb2b626a329f4de04dddc7c2571c1d204b6be2b6cc9eb87ed926c82ee47b", size = 6042, upload-time = "2024-09-13T09:48:59.681Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e6/fc55aec3e78e34d7f226a60719d1e01da8d4a78c2743ec4716af2c74ca0d/reflex_text_loop-0.0.1-py3-none-any.whl", hash = "sha256:37772b301ca87b8195fd37c09e848299bafa8484e104b9c14d5e1182b2cf40b7", size = 6426, upload-time = "2024-09-13T09:48:57.768Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"