From 2b09d7613127b46cfee1043f47a14e39e469b843 Mon Sep 17 00:00:00 2001 From: death916 Date: Wed, 5 Nov 2025 04:28:37 -0800 Subject: [PATCH] news --- deathclock/deathclock.py | 114 ++++++++++++++++++++++++++++----------- pyproject.toml | 1 + utils/news.py | 75 ++++++++++++++------------ uv.lock | 14 +++++ 4 files changed, 138 insertions(+), 66 deletions(-) diff --git a/deathclock/deathclock.py b/deathclock/deathclock.py index fefe2fd..2be5075 100755 --- a/deathclock/deathclock.py +++ b/deathclock/deathclock.py @@ -11,12 +11,12 @@ from typing import Any, Dict, List import reflex as rx + from utils.news import News from utils.scores import NBAScores, mlbScores, nflScores - -# --- Import your Weather utility --- from utils.weather import Weather + # --- Constants --- WEATHER_IMAGE_PATH = "/weather.jpg" # Web path in assets folder WEATHER_FETCH_INTERVAL = 360 @@ -28,12 +28,13 @@ logging.basicConfig( class State(rx.State): # --- State Variables --- - current_time: str = ( - "" # Note: rx.moment replaces the need for this if used for display - ) + current_time: str = "" alarm_time: str = "" - alarms: list = [] + alarms: List = [] news: List[Dict[str, Any]] = [] + current_news_index: int = 0 + news_rotate_interval: int = 10 + news_text: str = "" nba_scores: List[Dict[str, Any]] = [] mlb_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 logging.info("Triggering background tasks: Weather") # 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 --- @@ -140,31 +146,46 @@ class State(rx.State): ) await asyncio.sleep(500) - """@rx.event(background=True) - async def fetch_news(self): - # Fetches news periodically - # Placeholder for the actual news fetching logic - while True: - try: - logging.info("Fetching news...") - news_items = await self._news_client.get_news() - if not news_items: - logging.warning("No news items fetched.") - async with self: - self.news = [] - yield # Update frontend - else: - logging.info(f"Fetched {len(news_items)} news items.") - async with self: - self.news = news_items - yield + @rx.event(background=True) + async def fetch_news(self): + while True: + try: + logging.info("Fetching news...") + news_items = await self._news_client.get_news() + if not news_items: + logging.warning("No news items fetched.") + async with self: + self.news = [] + self.current_news_index = 0 + yield + else: + logging.info(f"Fetched {len(news_items)} news items.") + async with self: + self.news = news_items + self.current_news_index = 0 + yield - except Exception as e: - logging.error( - f"Error in fetch_news background task: {e}", exc_info=True - ) - await asyncio.sleep(500) -""" + except Exception as e: + logging.error( + f"Error in fetch_news background task: {e}", exc_info=True + ) + 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 --- @rx.event(background=True) @@ -350,6 +371,36 @@ def index() -> rx.Component: 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 page = rx.container( # pyright: ignore[reportReturnType] rx.theme_panel(default_open=False), @@ -357,6 +408,7 @@ def index() -> rx.Component: rx.vstack( clock_button, main_flex, + news_card, align="center", spacing="4", ) diff --git a/pyproject.toml b/pyproject.toml index d58e698..55e798c 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "mlb-statsapi>=1.8.1", "reflex>=0.6.8", "aiohttp>=3.11.18", + "reflex-text-loop>=0.0.1", ] [dependency-groups] diff --git a/utils/news.py b/utils/news.py index 23b66c5..22c7965 100755 --- a/utils/news.py +++ b/utils/news.py @@ -6,19 +6,19 @@ from time import localtime, strftime import socket import aiohttp + def print_time(): print(strftime("%B %d, %I:%M %p", localtime())) + class News: def __init__(self): - self._news_dict = {} - self._news_dict_length = 0 - socket.setdefaulttimeout(10) # Set default timeout for socket operations - + socket.setdefaulttimeout(10) # Set default timeout for socket operations + async def _fetch_feed(self, session, feed): """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: # Add timeout to the request timeout = aiohttp.ClientTimeout(total=5) @@ -26,30 +26,36 @@ class News: if response.status != 200: print(f"Skip feed {feed}: status {response.status}") return [] - + text = await response.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}") return [] - + feed_entries = [] # Limit the number of entries parsed for i, post in enumerate(d.entries): if i >= max_entries: - break # Stop parsing if we've reached the limit - - feed_entries.append({ - 'title': post.title, - 'source': d.feed.title if hasattr(d.feed, 'title') else 'Unknown', - 'publish_date': post.published if hasattr(post, 'published') else '', - 'summary': post.summary if hasattr(post, 'summary') else '' - }) - + break # Stop parsing if we've reached the limit + + feed_entries.append( + { + "title": post.title, + "source": d.feed.title + 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}") return feed_entries - + except aiohttp.ClientError as e: print(f"Error processing feed {feed}: {e}") return [] @@ -62,7 +68,7 @@ class News: feeds = [] self._news_dict = {} self._news_dict_length = 0 - + try: async with aiofiles.open("feeds.txt", "r") as f: async for line in f: @@ -70,38 +76,37 @@ class News: except Exception as e: print(f"Error reading feeds.txt: {e}") return {} - + # Limit the number of feeds to process at once if len(feeds) > 10: feeds = random.sample(feeds, 10) - + print("Getting news entries...") timeout = aiohttp.ClientTimeout(total=15) async with aiohttp.ClientSession(timeout=timeout) as session: tasks = [self._fetch_feed(session, feed) for feed in feeds] all_feed_entries_list = await asyncio.gather(*tasks, return_exceptions=True) - + all_entries = [] for result in all_feed_entries_list: if isinstance(result, list) and result: all_entries.extend(result) - + if not all_entries: print("No entries collected") - return {} - + return [] + if len(all_entries) > 30: all_entries = random.sample(all_entries, 30) - - for entry in all_entries: - self._news_dict[entry['title']] = entry - + try: async with aiofiles.open("news.txt", "w") as f: print("Writing news to file...") - for entry in self._news_dict.values(): - await f.write(f"[{entry['publish_date']}] {entry['source']}: {entry['title']}\n") + for entry in all_entries: + await f.write( + f"[{entry['publish_date']}] {entry['source']}: {entry['title']}\n" + ) except Exception as e: print(f"Error writing to news.txt: {e}") - - return self._news_dict \ No newline at end of file + + return all_entries diff --git a/uv.lock b/uv.lock index de930bc..6ad333a 100755 --- a/uv.lock +++ b/uv.lock @@ -268,6 +268,7 @@ dependencies = [ { name = "nba-api" }, { name = "playwright" }, { name = "reflex" }, + { name = "reflex-text-loop" }, { name = "requests" }, { name = "rich" }, ] @@ -287,6 +288,7 @@ requires-dist = [ { name = "nba-api", specifier = ">=1.6.1,<2" }, { name = "playwright", specifier = ">=1.49.1,<2" }, { name = "reflex", specifier = ">=0.6.8" }, + { name = "reflex-text-loop", specifier = ">=0.0.1" }, { name = "requests", specifier = ">=2.31.0,<3" }, { 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" }, ] +[[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]] name = "requests" version = "2.32.3"