From 8dee5fed1fc93faa988e7cc4b217cfac28fd9e37 Mon Sep 17 00:00:00 2001 From: CIAQ0404 <120072454+CIAQ0404@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:56:08 -0400 Subject: [PATCH 1/5] Baseball Scores for Ticker I added files which take scores from a mlb website and orders them and shows the scores of current games and when other games will be played. --- mlb-feed/__pycache__/main.cpython-314.pyc | Bin 0 -> 4758 bytes mlb-feed/concerto-mlb.service.txt | 13 ++++ mlb-feed/main.py | 91 ++++++++++++++++++++++ mlb-feed/requirements.txt | 2 + mlb-feed/start.sh | 5 ++ 5 files changed, 111 insertions(+) create mode 100644 mlb-feed/__pycache__/main.cpython-314.pyc create mode 100644 mlb-feed/concerto-mlb.service.txt create mode 100644 mlb-feed/main.py create mode 100644 mlb-feed/requirements.txt create mode 100644 mlb-feed/start.sh diff --git a/mlb-feed/__pycache__/main.cpython-314.pyc b/mlb-feed/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef0a03575919185a05037b1c7786915502c2c6a2 GIT binary patch literal 4758 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 str: + teams = game.get("teams", {}) + away = teams.get("away", {}) + home = teams.get("home", {}) + + away_team = away.get("team", {}).get("name", "Away") + home_team = home.get("team", {}).get("name", "Home") + + away_score = away.get("score") + home_score = home.get("score") + + status = game.get("status", {}) + detailed_state = status.get("detailedState", "Scheduled") + abstract_state = status.get("abstractGameState", "Preview") + + game_datetime = game.get("gameDate") + display_time = "TBD" + + if game_datetime: + try: + dt = datetime.datetime.fromisoformat(game_datetime.replace("Z", "+00:00")) + display_time = dt.astimezone().strftime("%I:%M %p").lstrip("0") + except ValueError: + pass + + if abstract_state == "Final": + return f"FINAL: {away_team} {away_score}, {home_team} {home_score}" + elif abstract_state == "Live": + return f"LIVE: {away_team} {away_score}, {home_team} {home_score} ({detailed_state})" + else: + return f"{display_time}: {away_team} at {home_team}" + + +def get_mlb_games() -> List[str]: + today = datetime.datetime.now().strftime("%Y-%m-%d") + + response = requests.get( + MLB_SCHEDULE_URL, + params={ + "sportId": 1, + "date": today, + "hydrate": "linescore,team,flags", + }, + timeout=20, + ) + + data = response.json() + dates = data.get("dates", []) + if not dates: + return ["No MLB games scheduled today."] + + games = dates[0].get("games", []) + if not games: + return ["No MLB games scheduled today."] + + lines = [format_game_line(game) for game in games[:12]] + return lines + + +@app.get("/mlb.json") +def read_mlb_scores() -> List[Dict[str, str]] | Dict[str, str]: + now = datetime.datetime.now() + start = now.replace(hour=0, minute=0, second=0, microsecond=0) + end = now.replace(hour=23, minute=59, second=59, microsecond=999999) + + try: + game_lines = get_mlb_games() + except Exception as e: + return {"Error": "Response Failed", "info": str(e)} + + html = " | ".join(game_lines) + + return [{ + "name": "MLB Scores", + "type": "RichText", + "render_as": "html", + "start_time": start.strftime("%Y-%m-%dT%H:%M:%SZ"), + "end_time": end.strftime("%Y-%m-%dT%H:%M:%SZ"), + "text": html + }] \ No newline at end of file diff --git a/mlb-feed/requirements.txt b/mlb-feed/requirements.txt new file mode 100644 index 000000000..b07ade6ba --- /dev/null +++ b/mlb-feed/requirements.txt @@ -0,0 +1,2 @@ +fastapi[standard] +requests \ No newline at end of file diff --git a/mlb-feed/start.sh b/mlb-feed/start.sh new file mode 100644 index 000000000..3d35bb6b9 --- /dev/null +++ b/mlb-feed/start.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +. .venv/bin/activate +fastapi run --host 0.0.0.0 --port 43679 +deactivate \ No newline at end of file From 3a7d1dad1cb6fa1d905b9ef69112e8e4f8109f4d Mon Sep 17 00:00:00 2001 From: CIAQ0404 <120072454+CIAQ0404@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:59:35 -0400 Subject: [PATCH 2/5] Delete mlb-feed directory --- mlb-feed/__pycache__/main.cpython-314.pyc | Bin 4758 -> 0 bytes mlb-feed/concerto-mlb.service.txt | 13 ---- mlb-feed/main.py | 91 ---------------------- mlb-feed/requirements.txt | 2 - mlb-feed/start.sh | 5 -- 5 files changed, 111 deletions(-) delete mode 100644 mlb-feed/__pycache__/main.cpython-314.pyc delete mode 100644 mlb-feed/concerto-mlb.service.txt delete mode 100644 mlb-feed/main.py delete mode 100644 mlb-feed/requirements.txt delete mode 100644 mlb-feed/start.sh diff --git a/mlb-feed/__pycache__/main.cpython-314.pyc b/mlb-feed/__pycache__/main.cpython-314.pyc deleted file mode 100644 index ef0a03575919185a05037b1c7786915502c2c6a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4758 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 str: - teams = game.get("teams", {}) - away = teams.get("away", {}) - home = teams.get("home", {}) - - away_team = away.get("team", {}).get("name", "Away") - home_team = home.get("team", {}).get("name", "Home") - - away_score = away.get("score") - home_score = home.get("score") - - status = game.get("status", {}) - detailed_state = status.get("detailedState", "Scheduled") - abstract_state = status.get("abstractGameState", "Preview") - - game_datetime = game.get("gameDate") - display_time = "TBD" - - if game_datetime: - try: - dt = datetime.datetime.fromisoformat(game_datetime.replace("Z", "+00:00")) - display_time = dt.astimezone().strftime("%I:%M %p").lstrip("0") - except ValueError: - pass - - if abstract_state == "Final": - return f"FINAL: {away_team} {away_score}, {home_team} {home_score}" - elif abstract_state == "Live": - return f"LIVE: {away_team} {away_score}, {home_team} {home_score} ({detailed_state})" - else: - return f"{display_time}: {away_team} at {home_team}" - - -def get_mlb_games() -> List[str]: - today = datetime.datetime.now().strftime("%Y-%m-%d") - - response = requests.get( - MLB_SCHEDULE_URL, - params={ - "sportId": 1, - "date": today, - "hydrate": "linescore,team,flags", - }, - timeout=20, - ) - - data = response.json() - dates = data.get("dates", []) - if not dates: - return ["No MLB games scheduled today."] - - games = dates[0].get("games", []) - if not games: - return ["No MLB games scheduled today."] - - lines = [format_game_line(game) for game in games[:12]] - return lines - - -@app.get("/mlb.json") -def read_mlb_scores() -> List[Dict[str, str]] | Dict[str, str]: - now = datetime.datetime.now() - start = now.replace(hour=0, minute=0, second=0, microsecond=0) - end = now.replace(hour=23, minute=59, second=59, microsecond=999999) - - try: - game_lines = get_mlb_games() - except Exception as e: - return {"Error": "Response Failed", "info": str(e)} - - html = " | ".join(game_lines) - - return [{ - "name": "MLB Scores", - "type": "RichText", - "render_as": "html", - "start_time": start.strftime("%Y-%m-%dT%H:%M:%SZ"), - "end_time": end.strftime("%Y-%m-%dT%H:%M:%SZ"), - "text": html - }] \ No newline at end of file diff --git a/mlb-feed/requirements.txt b/mlb-feed/requirements.txt deleted file mode 100644 index b07ade6ba..000000000 --- a/mlb-feed/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi[standard] -requests \ No newline at end of file diff --git a/mlb-feed/start.sh b/mlb-feed/start.sh deleted file mode 100644 index 3d35bb6b9..000000000 --- a/mlb-feed/start.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -. .venv/bin/activate -fastapi run --host 0.0.0.0 --port 43679 -deactivate \ No newline at end of file From 54d1a33169a4a3efbfb447c37b693fce09a0d6e4 Mon Sep 17 00:00:00 2001 From: CIAQ0404 <120072454+CIAQ0404@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:02:01 -0400 Subject: [PATCH 3/5] Added MLB scores for the Ticker Added code which takes scores from an MLB website and puts those scores on a website with a created port number --- .../mlb-feed/__pycache__/main.cpython-314.pyc | Bin 0 -> 4758 bytes feeds/mlb-feed/concerto-mlb.service.txt | 13 +++ feeds/mlb-feed/main.py | 91 ++++++++++++++++++ feeds/mlb-feed/requirements.txt | 2 + feeds/mlb-feed/start.sh | 5 + 5 files changed, 111 insertions(+) create mode 100644 feeds/mlb-feed/__pycache__/main.cpython-314.pyc create mode 100644 feeds/mlb-feed/concerto-mlb.service.txt create mode 100644 feeds/mlb-feed/main.py create mode 100644 feeds/mlb-feed/requirements.txt create mode 100644 feeds/mlb-feed/start.sh diff --git a/feeds/mlb-feed/__pycache__/main.cpython-314.pyc b/feeds/mlb-feed/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef0a03575919185a05037b1c7786915502c2c6a2 GIT binary patch literal 4758 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 str: + teams = game.get("teams", {}) + away = teams.get("away", {}) + home = teams.get("home", {}) + + away_team = away.get("team", {}).get("name", "Away") + home_team = home.get("team", {}).get("name", "Home") + + away_score = away.get("score") + home_score = home.get("score") + + status = game.get("status", {}) + detailed_state = status.get("detailedState", "Scheduled") + abstract_state = status.get("abstractGameState", "Preview") + + game_datetime = game.get("gameDate") + display_time = "TBD" + + if game_datetime: + try: + dt = datetime.datetime.fromisoformat(game_datetime.replace("Z", "+00:00")) + display_time = dt.astimezone().strftime("%I:%M %p").lstrip("0") + except ValueError: + pass + + if abstract_state == "Final": + return f"FINAL: {away_team} {away_score}, {home_team} {home_score}" + elif abstract_state == "Live": + return f"LIVE: {away_team} {away_score}, {home_team} {home_score} ({detailed_state})" + else: + return f"{display_time}: {away_team} at {home_team}" + + +def get_mlb_games() -> List[str]: + today = datetime.datetime.now().strftime("%Y-%m-%d") + + response = requests.get( + MLB_SCHEDULE_URL, + params={ + "sportId": 1, + "date": today, + "hydrate": "linescore,team,flags", + }, + timeout=20, + ) + + data = response.json() + dates = data.get("dates", []) + if not dates: + return ["No MLB games scheduled today."] + + games = dates[0].get("games", []) + if not games: + return ["No MLB games scheduled today."] + + lines = [format_game_line(game) for game in games[:12]] + return lines + + +@app.get("/mlb.json") +def read_mlb_scores() -> List[Dict[str, str]] | Dict[str, str]: + now = datetime.datetime.now() + start = now.replace(hour=0, minute=0, second=0, microsecond=0) + end = now.replace(hour=23, minute=59, second=59, microsecond=999999) + + try: + game_lines = get_mlb_games() + except Exception as e: + return {"Error": "Response Failed", "info": str(e)} + + html = " | ".join(game_lines) + + return [{ + "name": "MLB Scores", + "type": "RichText", + "render_as": "html", + "start_time": start.strftime("%Y-%m-%dT%H:%M:%SZ"), + "end_time": end.strftime("%Y-%m-%dT%H:%M:%SZ"), + "text": html + }] \ No newline at end of file diff --git a/feeds/mlb-feed/requirements.txt b/feeds/mlb-feed/requirements.txt new file mode 100644 index 000000000..b07ade6ba --- /dev/null +++ b/feeds/mlb-feed/requirements.txt @@ -0,0 +1,2 @@ +fastapi[standard] +requests \ No newline at end of file diff --git a/feeds/mlb-feed/start.sh b/feeds/mlb-feed/start.sh new file mode 100644 index 000000000..3d35bb6b9 --- /dev/null +++ b/feeds/mlb-feed/start.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +. .venv/bin/activate +fastapi run --host 0.0.0.0 --port 43679 +deactivate \ No newline at end of file From 9eba0b03c6296ab039f6475534fa6ce82cb06d55 Mon Sep 17 00:00:00 2001 From: CIAQ0404 <120072454+CIAQ0404@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:40:45 -0400 Subject: [PATCH 4/5] Add calendar feed pipeline for Concerto event display Add calendar feed support for Concerto by introducing a two-step pipeline: - fetch RPI event data into events.json - generate calendar.html from cleaned event data This keeps event retrieval separate from display generation and makes the calendar easier to update and maintain. --- calendar/calander.py | 140 ++++++++++++++ calendar/calender_display.py | 356 +++++++++++++++++++++++++++++++++++ calendar/fetch_events.py | 63 +++++++ 3 files changed, 559 insertions(+) create mode 100644 calendar/calander.py create mode 100644 calendar/calender_display.py create mode 100644 calendar/fetch_events.py diff --git a/calendar/calander.py b/calendar/calander.py new file mode 100644 index 000000000..82688760a --- /dev/null +++ b/calendar/calander.py @@ -0,0 +1,140 @@ +import json +from datetime import datetime + +INPUT_FILE = "events.json" +OUTPUT_FILE = "calendar.html" +MAX_EVENTS = 5 + + +def load_events(): + with open(INPUT_FILE, "r", encoding="utf-8") as f: + return json.load(f) + + +def format_event_html(event): + title = event.get("title", "Untitled Event") + date = event.get("date", "") + start_time = event.get("start_time", "") + end_time = event.get("end_time", "") + location = event.get("location", "No location listed") + description = event.get("description", "") + link = event.get("event_link", "") + + time_text = start_time + if end_time: + time_text += f" - {end_time}" + + if len(description) > 180: + description = description[:177] + "..." + + link_html = "" + if link: + link_html = f"

More Info

" + + return f""" +
+

{title}

+

Date: {date}

+

Time: {time_text}

+

Location: {location}

+

{description}

+ {link_html} +
+ """ + + +def build_html(data): + title = data.get("title", "Upcoming Events") + updated_at = data.get("updated_at", "") + events = data.get("events", [])[:MAX_EVENTS] + + events_html = "".join(format_event_html(event) for event in events) + + html = f""" + + + + + + {title} + + + +
+

{title}

+
Last updated: {updated_at}
+ {events_html} +
+ + +""" + return html + + +def main(): + data = load_events() + html = build_html(data) + + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + f.write(html) + + print(f"Created {OUTPUT_FILE}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/calendar/calender_display.py b/calendar/calender_display.py new file mode 100644 index 000000000..5404f6d2d --- /dev/null +++ b/calendar/calender_display.py @@ -0,0 +1,356 @@ +import requests +import json +import re +import time +from datetime import datetime, timedelta + +FEED_URL = "https://events.rpi.edu/feeder/main/eventsFeed.do?f=y&sort=dtstart.utc:asc&fexpr=(categories.href!=%22/public/.bedework/categories/Ongoing%22)%20and%20(entity_type=%22event%22%20or%20entity_type=%22todo%22)&skinName=list-json&setappvar=objName(bwObject)&count=10" +OUTPUT_HTML = "calendar.html" +REFRESH_SECONDS = 1800 # 30 minutes + + +def extract_bwobject(text): + match = re.search(r'var\s+bwObject\s*=\s*(\{.*\})\s*$', text, re.DOTALL) + if not match: + raise ValueError("Could not find bwObject in feed response.") + return match.group(1) + + +def fetch_events(): + response = requests.get(FEED_URL, timeout=20) + response.raise_for_status() + + raw_text = response.text + bwobject_text = extract_bwobject(raw_text) + bwobject = json.loads(bwobject_text) + + return bwobject.get("bwEventList", {}).get("events", []) + + +def parse_event_datetime(date_str, time_str): + if not date_str or not time_str: + return None + + formats = [ + "%B %d, %Y %I:%M %p", + "%b %d, %Y %I:%M %p", + ] + + combined = f"{date_str} {time_str}".strip() + for fmt in formats: + try: + return datetime.strptime(combined, fmt) + except ValueError: + continue + return None + + +def clean_text(text, max_len=None): + if not text: + return "" + text = re.sub(r"<[^>]+>", "", text) + text = re.sub(r"\s+", " ", text).strip() + if max_len and len(text) > max_len: + return text[: max_len - 3] + "..." + return text + + +def normalize_event(event): + start = event.get("start", {}) + end = event.get("end", {}) + location = event.get("location", {}) + + title = clean_text(event.get("summary", "Untitled Event")) + description = clean_text(event.get("description", ""), 180) + location_text = clean_text(location.get("address", "Location TBA")) + event_link = event.get("eventlink", "") + + start_date = start.get("longdate", "") + start_time = start.get("time", "") + end_date = end.get("longdate", start_date) + end_time = end.get("time", "") + + start_dt = parse_event_datetime(start_date, start_time) + end_dt = parse_event_datetime(end_date, end_time) + + all_day = str(start.get("allday", "false")).lower() == "true" + + return { + "title": title, + "description": description, + "location": location_text, + "event_link": event_link, + "start_date": start_date, + "start_time": start_time, + "end_date": end_date, + "end_time": end_time, + "start_dt": start_dt, + "end_dt": end_dt, + "all_day": all_day, + "formatted_date": clean_text(event.get("formattedDate", "")), + } + + +def choose_events(events): + now = datetime.now() + normalized = [normalize_event(e) for e in events] + + valid = [e for e in normalized if e["start_dt"] is not None] + valid.sort(key=lambda e: e["start_dt"]) + + current_event = None + upcoming = [] + + for event in valid: + start_dt = event["start_dt"] + end_dt = event["end_dt"] + + # If end time is missing, assume 2 hours after start + if end_dt is None and start_dt is not None: + end_dt = start_dt + timedelta(hours=2) + event["end_dt"] = end_dt + + if start_dt <= now <= end_dt: + current_event = event + elif start_dt > now: + upcoming.append(event) + + if current_event: + return "Happening Now", current_event, upcoming[:2] + + if upcoming: + return "Coming Up Next", upcoming[0], upcoming[1:3] + + return "No Upcoming Events", None, [] + + +def format_main_event(event, status_label): + if not event: + return f""" +
+
{status_label}
+

No upcoming events found

+

Please check back later.

+
+ """ + + time_line = "All Day" if event["all_day"] else f'{event["start_time"]} - {event["end_time"]}'.strip(" -") + description_html = f"

{event['description']}

" if event["description"] else "" + + return f""" +
+
{status_label}
+

{event["title"]}

+

Date: {event["start_date"]}

+

Time: {time_line}

+

Location: {event["location"]}

+ {description_html} +
+ """ + + +def format_sidebar_events(events): + if not events: + return "

No additional upcoming events.

" + + html_parts = ["

Also Coming Up

"] + for event in events: + time_line = "All Day" if event["all_day"] else event["start_time"] + html_parts.append(f""" +
+
{event["title"]}
+
{event["start_date"]}
+
{time_line}
+
{event["location"]}
+
+ """) + html_parts.append("
") + return "".join(html_parts) + + +def build_html(status_label, main_event, sidebar_events): + updated_at = datetime.now().strftime("%m/%d/%Y %I:%M %p") + main_html = format_main_event(main_event, status_label) + sidebar_html = format_sidebar_events(sidebar_events) + + html = f""" + + + + + + RPI Events Display + + + +
+
+
+
+

RPI Events

+

Current and upcoming campus events

+
+
+ +
+ {main_html} + {sidebar_html} +
+
+ + +
+ + +""" + return html + + +def write_html(html): + with open(OUTPUT_HTML, "w", encoding="utf-8") as f: + f.write(html) + + +def update_display(): + events = fetch_events() + status_label, main_event, sidebar_events = choose_events(events) + html = build_html(status_label, main_event, sidebar_events) + write_html(html) + print(f"Updated {OUTPUT_HTML} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + +def main(): + while True: + try: + update_display() + except Exception as e: + print("Error updating display:", e) + + time.sleep(REFRESH_SECONDS) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/calendar/fetch_events.py b/calendar/fetch_events.py new file mode 100644 index 000000000..de531fbd8 --- /dev/null +++ b/calendar/fetch_events.py @@ -0,0 +1,63 @@ +import requests +import json +import re +from datetime import datetime + +FEED_URL = "https://events.rpi.edu/feeder/main/eventsFeed.do?f=y&sort=dtstart.utc:asc&fexpr=(categories.href!=%22/public/.bedework/categories/Ongoing%22)%20and%20(entity_type=%22event%22%20or%20entity_type=%22todo%22)&skinName=list-json&setappvar=objName(bwObject)&count=10" +OUTPUT_FILE = "events.json" + + +def extract_bwobject(text): + """ + Extract the JavaScript object from: + var bwObject = {...} + """ + match = re.search(r'var\s+bwObject\s*=\s*(\{.*\})\s*$', text, re.DOTALL) + if not match: + raise ValueError("Could not find bwObject in feed response.") + return match.group(1) + + +def clean_event(event): + start = event.get("start", {}) + end = event.get("end", {}) + location = event.get("location", {}) + + return { + "title": event.get("summary", ""), + "formatted_date": event.get("formattedDate", ""), + "date": start.get("longdate", ""), + "start_time": start.get("time", ""), + "end_time": end.get("time", ""), + "all_day": str(start.get("allday", "false")).lower() == "true", + "location": location.get("address", ""), + "description": event.get("description", ""), + "event_link": event.get("eventlink", ""), + "categories": event.get("categories", []) + } + + +def main(): + response = requests.get(FEED_URL, timeout=15) + response.raise_for_status() + + raw_text = response.text + bwobject_text = extract_bwobject(raw_text) + bwobject = json.loads(bwobject_text) + + raw_events = bwobject.get("bwEventList", {}).get("events", []) + + cleaned = { + "title": "Upcoming Events", + "updated_at": datetime.now().isoformat(timespec="seconds"), + "events": [clean_event(event) for event in raw_events] + } + + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + json.dump(cleaned, f, indent=2, ensure_ascii=False) + + print(f"Saved {len(cleaned['events'])} events to {OUTPUT_FILE}") + + +if __name__ == "__main__": + main() \ No newline at end of file From d542a70650c2a4db35c6a5395f97f49cd3c1ba70 Mon Sep 17 00:00:00 2001 From: CIAQ0404 <120072454+CIAQ0404@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:41:14 -0400 Subject: [PATCH 5/5] Added Local news feed for Ticker Added local news feed from WTEN News10, for ticker, should show headlines. --- feeds/news_feed/main.py | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 feeds/news_feed/main.py diff --git a/feeds/news_feed/main.py b/feeds/news_feed/main.py new file mode 100644 index 000000000..640c42d8c --- /dev/null +++ b/feeds/news_feed/main.py @@ -0,0 +1,48 @@ +from typing import Dict, List +from fastapi import FastAPI +import datetime +import feedparser + +app = FastAPI() + +# Example local RSS feed (Albany Times Union) +RSS_URL = "https://www.news10.com/feed/" + +def get_news_headlines() -> List[str]: + feed = feedparser.parse(RSS_URL) + + headlines = [] + + for entry in feed.entries[:10]: + title = entry.get("title", "").strip() + if title: + headlines.append(title[:90] + "..." if len(title) > 90 else title) + + if not headlines: + return ["No news available"] + + return headlines + + +@app.get("/news.json") +def news_feed() -> List[Dict[str, str]] | Dict[str, str]: + now = datetime.datetime.now() + start = now.replace(hour=0, minute=0, second=0, microsecond=0) + end = now.replace(hour=23, minute=59, second=59, microsecond=999999) + + try: + headlines = get_news_headlines() + except Exception as e: + return {"Error": "Failed to fetch news", "info": str(e)} + + # Format for ticker (IMPORTANT) + text = "Capital Region News: " + " | ".join(headlines) + + return [{ + "name": "Local News", + "type": "RichText", + "render_as": "html", + "start_time": start.strftime("%Y-%m-%dT%H:%M:%SZ"), + "end_time": end.strftime("%Y-%m-%dT%H:%M:%SZ"), + "text": text + }] \ No newline at end of file