From c91ecf60a616e29265b956e5aa8c794361022c1d Mon Sep 17 00:00:00 2001 From: AmaanShikalgar Date: Sun, 19 Jul 2026 18:56:03 +0530 Subject: [PATCH 01/10] feat: post all livestream URLs upfront instead of per-session --- .../programme_notifications/cog.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/europython_discord/programme_notifications/cog.py b/src/europython_discord/programme_notifications/cog.py index cc9160c..12135f4 100644 --- a/src/europython_discord/programme_notifications/cog.py +++ b/src/europython_discord/programme_notifications/cog.py @@ -40,10 +40,30 @@ async def on_ready(self) -> None: await self.purge_all_room_channels() _logger.debug(f"Simulated start time: {self.config.simulated_start_time}") _logger.debug(f"Fast mode: {self.config.fast_mode}") + _logger.info("Posting livestream URLs for all rooms...") + await self.post_all_livestream_urls() _logger.info("Starting the session notifier...") self.notify_sessions.start() _logger.info("Cog 'Programme Notifications' is ready") + async def post_all_livestream_urls(self) -> None: + if self.livestream_connector.livestreams_by_room is None: + await self.livestream_connector.fetch_livestreams() + + for room_name in self.config.rooms_to_channel_names: + room_channel = self._get_room_channel(room_name) + if room_channel is None: + continue + + urls_by_date = self.livestream_connector.livestreams_by_room.get(room_name) + if not urls_by_date: + continue + + topic = "\n".join( + f"Stream {day.strftime('%A')}: {url}" for day, url in sorted(urls_by_date.items()) + ) + await room_channel.edit(topic=topic) + async def cog_load(self) -> None: """Start schedule updater task.""" _logger.info( @@ -105,11 +125,8 @@ async def notify_sessions(self) -> None: # send session notification message to room and main channel await main_notification_channel.send(embed=embed) - # update room's livestream URL + # send session notification message to room if room_channel is not None: - await room_channel.edit( - topic=f"Livestream: [YouTube]({livestream_url})" if livestream_url else "" - ) await room_channel.send( content=f"# Starting in 5 minutes @ {session.rooms[0]}", embed=embed, From 266db5761e762f402c90a2679c959f3997bde569 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Tue, 21 Jul 2026 18:56:27 +0200 Subject: [PATCH 02/10] Refactor: Move `post_all_livestream_urls` to the correct position This method is analogous to `notify_sessions`, so it should be located next to it, instead of between `on_ready` and `cog_load`. --- .../programme_notifications/cog.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/europython_discord/programme_notifications/cog.py b/src/europython_discord/programme_notifications/cog.py index 12135f4..260a4bb 100644 --- a/src/europython_discord/programme_notifications/cog.py +++ b/src/europython_discord/programme_notifications/cog.py @@ -46,24 +46,6 @@ async def on_ready(self) -> None: self.notify_sessions.start() _logger.info("Cog 'Programme Notifications' is ready") - async def post_all_livestream_urls(self) -> None: - if self.livestream_connector.livestreams_by_room is None: - await self.livestream_connector.fetch_livestreams() - - for room_name in self.config.rooms_to_channel_names: - room_channel = self._get_room_channel(room_name) - if room_channel is None: - continue - - urls_by_date = self.livestream_connector.livestreams_by_room.get(room_name) - if not urls_by_date: - continue - - topic = "\n".join( - f"Stream {day.strftime('%A')}: {url}" for day, url in sorted(urls_by_date.items()) - ) - await room_channel.edit(topic=topic) - async def cog_load(self) -> None: """Start schedule updater task.""" _logger.info( @@ -94,6 +76,24 @@ async def fetch_livestreams(self) -> None: await self.livestream_connector.fetch_livestreams() _logger.info("Finished the periodic livestream update.") + async def post_all_livestream_urls(self) -> None: + if self.livestream_connector.livestreams_by_room is None: + await self.livestream_connector.fetch_livestreams() + + for room_name in self.config.rooms_to_channel_names: + room_channel = self._get_room_channel(room_name) + if room_channel is None: + continue + + urls_by_date = self.livestream_connector.livestreams_by_room.get(room_name) + if not urls_by_date: + continue + + topic = "\n".join( + f"Stream {day.strftime('%A')}: {url}" for day, url in sorted(urls_by_date.items()) + ) + await room_channel.edit(topic=topic) + @tasks.loop() async def notify_sessions(self) -> None: # determine sessions to send notifications for From d598b174e9a7f095b231936ebe9a092c8f206016 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Tue, 21 Jul 2026 18:57:32 +0200 Subject: [PATCH 03/10] Refactor: Rename `post_all_livestream_urls` to `notify_livestreams` This method is analogous to `notify_sessions`, so it should be named similarly. --- src/europython_discord/programme_notifications/cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/europython_discord/programme_notifications/cog.py b/src/europython_discord/programme_notifications/cog.py index 260a4bb..e40fccf 100644 --- a/src/europython_discord/programme_notifications/cog.py +++ b/src/europython_discord/programme_notifications/cog.py @@ -41,7 +41,7 @@ async def on_ready(self) -> None: _logger.debug(f"Simulated start time: {self.config.simulated_start_time}") _logger.debug(f"Fast mode: {self.config.fast_mode}") _logger.info("Posting livestream URLs for all rooms...") - await self.post_all_livestream_urls() + await self.notify_livestreams() _logger.info("Starting the session notifier...") self.notify_sessions.start() _logger.info("Cog 'Programme Notifications' is ready") @@ -76,7 +76,7 @@ async def fetch_livestreams(self) -> None: await self.livestream_connector.fetch_livestreams() _logger.info("Finished the periodic livestream update.") - async def post_all_livestream_urls(self) -> None: + async def notify_livestreams(self) -> None: if self.livestream_connector.livestreams_by_room is None: await self.livestream_connector.fetch_livestreams() From e55740a8e491fee4e8bb45a37fae7d048c0029ca Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Tue, 21 Jul 2026 19:02:14 +0200 Subject: [PATCH 04/10] Run `notify_livestreams` in a loop instead of once on startup Like the schedule, the livestream URLs can change while the program is running. That's why we fetch them regularly. So the task `notify_livestreams` should run regularly, like `notify_sessions`. --- .../programme_notifications/cog.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/europython_discord/programme_notifications/cog.py b/src/europython_discord/programme_notifications/cog.py index e40fccf..2f8a74e 100644 --- a/src/europython_discord/programme_notifications/cog.py +++ b/src/europython_discord/programme_notifications/cog.py @@ -40,23 +40,25 @@ async def on_ready(self) -> None: await self.purge_all_room_channels() _logger.debug(f"Simulated start time: {self.config.simulated_start_time}") _logger.debug(f"Fast mode: {self.config.fast_mode}") - _logger.info("Posting livestream URLs for all rooms...") - await self.notify_livestreams() + + _logger.info("Starting the livestream notifier...") + self.notify_livestreams.start() _logger.info("Starting the session notifier...") self.notify_sessions.start() _logger.info("Cog 'Programme Notifications' is ready") async def cog_load(self) -> None: """Start schedule updater task.""" - _logger.info( - "Starting the schedule updater and setting the interval for the session notifier..." - ) + _logger.info("Starting the schedule updater and setting the interval for the notifiers...") self.fetch_schedule.start() self.fetch_livestreams.start() + self.notify_livestreams.change_interval( + seconds=2 if self.config.fast_mode and self.config.simulated_start_time else 60 + ) self.notify_sessions.change_interval( seconds=2 if self.config.fast_mode and self.config.simulated_start_time else 60 ) - _logger.info("Schedule updater started and interval set for the session notifier") + _logger.info("Schedule updater started and interval set for the notifiers") async def cog_unload(self) -> None: """Stop all tasks.""" @@ -76,6 +78,7 @@ async def fetch_livestreams(self) -> None: await self.livestream_connector.fetch_livestreams() _logger.info("Finished the periodic livestream update.") + @tasks.loop() async def notify_livestreams(self) -> None: if self.livestream_connector.livestreams_by_room is None: await self.livestream_connector.fetch_livestreams() From 3739094144941fdcb18bc2a954173041f2442283 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Tue, 21 Jul 2026 19:04:50 +0200 Subject: [PATCH 05/10] Don't trigger a fetch if no livestreams are found The livestreams are fetched regularly (every 5 minutes). Let's just wait for the regular fetch instead of triggering a second one. Also, it might be that there really are no livestreams to fetch. --- src/europython_discord/programme_notifications/cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/europython_discord/programme_notifications/cog.py b/src/europython_discord/programme_notifications/cog.py index 2f8a74e..7e9d6f4 100644 --- a/src/europython_discord/programme_notifications/cog.py +++ b/src/europython_discord/programme_notifications/cog.py @@ -81,7 +81,7 @@ async def fetch_livestreams(self) -> None: @tasks.loop() async def notify_livestreams(self) -> None: if self.livestream_connector.livestreams_by_room is None: - await self.livestream_connector.fetch_livestreams() + return for room_name in self.config.rooms_to_channel_names: room_channel = self._get_room_channel(room_name) From b613f8636ff50b166f64cec301c31a0d14cc4078 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Tue, 21 Jul 2026 19:08:03 +0200 Subject: [PATCH 06/10] Only obtain livestreams_by_room once This change is quite pedantic, but I want to get this information from the livestream_connector exactly once. Otherwise, it could (theoretically) change between the first and second access. --- src/europython_discord/programme_notifications/cog.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/europython_discord/programme_notifications/cog.py b/src/europython_discord/programme_notifications/cog.py index 7e9d6f4..5a337c3 100644 --- a/src/europython_discord/programme_notifications/cog.py +++ b/src/europython_discord/programme_notifications/cog.py @@ -80,7 +80,8 @@ async def fetch_livestreams(self) -> None: @tasks.loop() async def notify_livestreams(self) -> None: - if self.livestream_connector.livestreams_by_room is None: + livestreams_by_room = self.livestream_connector.livestreams_by_room + if livestreams_by_room is None: return for room_name in self.config.rooms_to_channel_names: @@ -88,7 +89,7 @@ async def notify_livestreams(self) -> None: if room_channel is None: continue - urls_by_date = self.livestream_connector.livestreams_by_room.get(room_name) + urls_by_date = livestreams_by_room.get(room_name) if not urls_by_date: continue From 315e814515a65a20de8778f35f44523d815e8cd0 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Tue, 21 Jul 2026 19:13:15 +0200 Subject: [PATCH 07/10] Restore previous livestream format Before adding this feature, the format was `Livestream: [YouTube]()`, using Markdown to display a lable instead of the plain URL. Let's keep it like that and only add the day. --- src/europython_discord/programme_notifications/cog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/europython_discord/programme_notifications/cog.py b/src/europython_discord/programme_notifications/cog.py index 5a337c3..a266360 100644 --- a/src/europython_discord/programme_notifications/cog.py +++ b/src/europython_discord/programme_notifications/cog.py @@ -93,8 +93,10 @@ async def notify_livestreams(self) -> None: if not urls_by_date: continue + # '%A': Weekday, e.g. 'Monday' topic = "\n".join( - f"Stream {day.strftime('%A')}: {url}" for day, url in sorted(urls_by_date.items()) + f"Livestream {day.strftime('%A')}: [YouTube]({url})" + for day, url in sorted(urls_by_date.items()) ) await room_channel.edit(topic=topic) From fd8e46e09db68cc6fff9c10911b2d6bdf82770c6 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Tue, 21 Jul 2026 19:17:32 +0200 Subject: [PATCH 08/10] Always use English weekdays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `date.strftime('%A')` returns the "Weekday as locale’s full name" (quote from https://docs.python.org/3/library/datetime.html#format-codes). We always want English output, even if the bot host is configured in another locale. --- .../programme_notifications/cog.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/europython_discord/programme_notifications/cog.py b/src/europython_discord/programme_notifications/cog.py index a266360..385598a 100644 --- a/src/europython_discord/programme_notifications/cog.py +++ b/src/europython_discord/programme_notifications/cog.py @@ -15,6 +15,16 @@ _logger = logging.getLogger(__name__) +_WEEKDAYS = { + 0: "Monday", + 1: "Tuesday", + 2: "Wednesday", + 3: "Thursday", + 4: "Friday", + 5: "Saturday", + 6: "Sunday", +} + class ProgrammeNotificationsCog(commands.Cog): def __init__(self, bot: Client, config: ProgrammeNotificationsConfig) -> None: @@ -93,9 +103,8 @@ async def notify_livestreams(self) -> None: if not urls_by_date: continue - # '%A': Weekday, e.g. 'Monday' topic = "\n".join( - f"Livestream {day.strftime('%A')}: [YouTube]({url})" + f"Livestream {_WEEKDAYS[day.timetuple().tm_wday]}: [YouTube]({url})" for day, url in sorted(urls_by_date.items()) ) await room_channel.edit(topic=topic) From 89f9e08d6e9ba4cdafaaec19e75d3bb6c0a8d03d Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Tue, 21 Jul 2026 19:50:01 +0200 Subject: [PATCH 09/10] ProgrammeConnector: Make current time publicly available This is necessary for the next step: Only sending livestream URLs a day in advance. (A cleaner approach would be to extract the 'current time' logic into a separate service, but this approach doesn't cause much chaos, so it's okay.) --- .../programme_notifications/programme_connector.py | 4 ++-- tests/program_notifications/test_program_connector.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/europython_discord/programme_notifications/programme_connector.py b/src/europython_discord/programme_notifications/programme_connector.py index 3fc339c..ad7d819 100644 --- a/src/europython_discord/programme_notifications/programme_connector.py +++ b/src/europython_discord/programme_notifications/programme_connector.py @@ -97,7 +97,7 @@ async def _get_schedule_from_cache(self) -> dict[date, list[Session]] | None: _logger.exception("Schedule cache file not found and no schedule is already loaded.") return None - async def _get_now(self) -> datetime: + async def get_current_time(self) -> datetime: """Get the current time in the conference timezone.""" if self._simulated_start_time: elapsed = datetime.now(tz=UTC) - self._real_start_time @@ -126,7 +126,7 @@ async def get_sessions_by_date(self, date_now: date) -> list[Session]: async def get_upcoming_sessions(self) -> list[Session]: # upcoming sessions are those that start in 5 minutes or less # and the start time is after the current time - now = await self._get_now() + now = await self.get_current_time() if self._simulated_start_time: _logger.debug(f"Simulated time now: {now}") diff --git a/tests/program_notifications/test_program_connector.py b/tests/program_notifications/test_program_connector.py index a329915..599cb2c 100644 --- a/tests/program_notifications/test_program_connector.py +++ b/tests/program_notifications/test_program_connector.py @@ -186,11 +186,11 @@ async def test_get_now_with_simulation(programme_connector): # ensure time is ticking between start and finish of this test await asyncio.sleep(0.001) - assert await programme_connector._get_now() > simulated_start_time + assert await programme_connector.get_current_time() > simulated_start_time async def test_get_now_without_simulation(programme_connector): - now = await programme_connector._get_now() + now = await programme_connector.get_current_time() # ensure time is ticking between start and finish of this test await asyncio.sleep(0.001) From c6d9bde8effd5a59d74b33cf8947eb0e56974470 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Tue, 21 Jul 2026 19:55:59 +0200 Subject: [PATCH 10/10] Only post livestream URLs a day in advance Also add the headline 'Channel for room {room_name}'. --- .../programme_notifications/cog.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/europython_discord/programme_notifications/cog.py b/src/europython_discord/programme_notifications/cog.py index 385598a..8bdb93b 100644 --- a/src/europython_discord/programme_notifications/cog.py +++ b/src/europython_discord/programme_notifications/cog.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from datetime import datetime +from datetime import datetime, timedelta from discord import Client, TextChannel from discord.ext import commands, tasks @@ -99,14 +99,23 @@ async def notify_livestreams(self) -> None: if room_channel is None: continue - urls_by_date = livestreams_by_room.get(room_name) - if not urls_by_date: + _urls_by_date = livestreams_by_room.get(room_name) + if not _urls_by_date: continue + urls_by_date = sorted(_urls_by_date.items()) # sort by date + + # don't notify of stream if the first stream is multiple days away + today = (await self.programme_connector.get_current_time()).date() + first_streaming_day = urls_by_date[0][0] + if first_streaming_day - today > timedelta(days=1): + topic = f"Channel for room {room_name}" + else: + livestreams_text = "\n".join( + f"Livestream {_WEEKDAYS[day.timetuple().tm_wday]}: [YouTube]({url})" + for day, url in urls_by_date + ) + topic = f"Channel for room {room_name}\n\n{livestreams_text}" - topic = "\n".join( - f"Livestream {_WEEKDAYS[day.timetuple().tm_wday]}: [YouTube]({url})" - for day, url in sorted(urls_by_date.items()) - ) await room_channel.edit(topic=topic) @tasks.loop()