feat: add human-readable datetime formatting and !lastshow command

IRC plugins now format datetimes as "Wed Mar 11, 10:00 PM EDT" instead
of raw ISO 8601. Configurable timezone defaults to America/New_York.

Adds !lastshow N command to both Limnoria and Sopel plugins, returning
track N from the previous week's show via existing API endpoints.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-12 05:31:50 -04:00
parent ae66242935
commit 9664b8225d
4 changed files with 167 additions and 2 deletions

View File

@@ -34,3 +34,12 @@ conf.registerGlobalValue(
"""IRC nicknames allowed to run admin commands (space-separated).""",
),
)
conf.registerGlobalValue(
NtrPlaylist,
"displayTimezone",
registry.String(
"America/New_York",
"""IANA timezone for displaying dates in IRC (e.g. America/New_York, America/Chicago).""",
),
)

View File

@@ -8,6 +8,8 @@ import logging
import re
import urllib.error
import urllib.request
from datetime import datetime
from zoneinfo import ZoneInfo
from supybot import callbacks
from supybot.commands import optional, wrap
@@ -73,6 +75,14 @@ def _api_post(base_url: str, path: str, token: str, body: dict | None = None) ->
# --- Formatting --------------------------------------------------------------
def format_dt(iso_string: str | None, tz_name: str = "America/New_York") -> str:
if not iso_string:
return "never"
dt = datetime.fromisoformat(iso_string)
local = dt.astimezone(ZoneInfo(tz_name))
return local.strftime("%a %b %-d, %-I:%M %p %Z")
_MAX_IRC_LINE = 430
@@ -195,6 +205,45 @@ class NtrPlaylist(callbacks.Plugin):
return
irc.reply(format_playlist(data))
@wrap([optional("text")])
def lastshow(self, irc, msg, args, text):
"""<position>
Returns a track from last week's show by position number.
"""
if not text or not text.strip():
irc.reply("Usage: !lastshow <position>")
return
try:
position = int(text.strip())
except ValueError:
irc.reply("Usage: !lastshow <position>")
return
base_url = self.registryValue("apiBaseUrl")
try:
shows = _api_get(base_url, "/shows?limit=2")
except ApiError as exc:
LOGGER.warning("API error for lastshow: %s", exc)
irc.reply(exc.detail)
return
if len(shows) < 2:
irc.reply("No previous show found")
return
prev_show_id = shows[1]["id"]
try:
data = _api_get(base_url, f"/shows/{prev_show_id}")
except ApiError as exc:
LOGGER.warning("API error for lastshow show %s: %s", prev_show_id, exc)
irc.reply(exc.detail)
return
tracks = data.get("tracks", [])
track = next((t for t in tracks if t.get("position") == position), None)
if not track:
episode = data.get("episode_number", "?")
irc.reply(f"No track at position {position} in episode {episode}")
return
irc.reply(format_track(track))
@wrap
def status(self, irc, msg, args):
"""takes no arguments
@@ -208,9 +257,10 @@ class NtrPlaylist(callbacks.Plugin):
LOGGER.warning("API error for status: %s", exc)
irc.reply(exc.detail)
return
tz = self.registryValue("displayTimezone")
status = data.get("status", "unknown")
poller = "alive" if data.get("poller_alive") else "dead"
last_fetch = data.get("last_fetch") or "never"
last_fetch = format_dt(data.get("last_fetch"), tz)
count = data.get("current_week_track_count", 0)
irc.reply(
f"Status: {status.upper()} | Poller: {poller} | Last fetch: {last_fetch} | Tracks this week: {count}"

View File

@@ -7,6 +7,8 @@ import json
import logging
import urllib.error
import urllib.request
from datetime import datetime
from zoneinfo import ZoneInfo
from sopel import plugin
from sopel.config import types
@@ -18,6 +20,7 @@ class NtrPlaylistSection(types.StaticSection):
api_base_url = types.ValidatedAttribute("api_base_url", default="http://127.0.0.1:8000")
admin_token = types.ValidatedAttribute("admin_token", default="")
admin_nicks = types.ListAttribute("admin_nicks")
display_timezone = types.ValidatedAttribute("display_timezone", default="America/New_York")
def setup(bot):
@@ -83,6 +86,14 @@ def _api_post(base_url: str, path: str, token: str, body: dict | None = None) ->
raise ApiError(0, "Cannot reach API") from e
def format_dt(iso_string: str | None, tz_name: str = "America/New_York") -> str:
if not iso_string:
return "never"
dt = datetime.fromisoformat(iso_string)
local = dt.astimezone(ZoneInfo(tz_name))
return local.strftime("%a %b %-d, %-I:%M %p %Z")
def format_track(track: dict) -> str:
pos = track.get("position", 0)
title = track.get("title", "")
@@ -188,6 +199,43 @@ def ntr_playlist(bot, trigger):
bot.say(format_playlist(data))
@plugin.command("lastshow")
def ntr_lastshow(bot, trigger):
raw = trigger.group(2)
if not raw or not raw.strip():
bot.say("Usage: !lastshow <position>")
return
try:
position = int(raw.strip())
except ValueError:
bot.say("Usage: !lastshow <position>")
return
base_url = bot.settings.ntr_playlist.api_base_url
try:
shows = _api_get(base_url, "/shows?limit=2")
except ApiError as e:
LOGGER.warning("API error for !lastshow: %s", e)
bot.say(e.detail)
return
if len(shows) < 2:
bot.say("No previous show found")
return
prev_show_id = shows[1]["id"]
try:
data = _api_get(base_url, f"/shows/{prev_show_id}")
except ApiError as e:
LOGGER.warning("API error for !lastshow show %s: %s", prev_show_id, e)
bot.say(e.detail)
return
tracks = data.get("tracks", [])
track = next((t for t in tracks if t.get("position") == position), None)
if not track:
episode = data.get("episode_number", "?")
bot.say(f"No track at position {position} in episode {episode}")
return
bot.say(format_track(track))
@plugin.command("status")
def ntr_status(bot, trigger):
base_url = bot.settings.ntr_playlist.api_base_url
@@ -197,9 +245,10 @@ def ntr_status(bot, trigger):
LOGGER.warning("API error for !status: %s", e)
bot.say(e.detail)
return
tz = bot.settings.ntr_playlist.display_timezone
status = data.get("status", "unknown")
poller = "alive" if data.get("poller_alive") else "dead"
last_fetch = data.get("last_fetch") or "never"
last_fetch = format_dt(data.get("last_fetch"), tz)
count = data.get("current_week_track_count", 0)
bot.say(f"Status: {status.upper()} | Poller: {poller} | Last fetch: {last_fetch} | Tracks this week: {count}")