feat: metadata enrichment for alerts and subscription plugins
Alert backends now populate structured `extra` field with engagement metrics (views, stars, votes, etc.) instead of embedding them in titles. Subscription plugins show richer announcements: Twitch viewer counts, YouTube views/likes/dates, RSS published dates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ _spec.loader.exec_module(_mod)
|
||||
from plugins.youtube import ( # noqa: E402
|
||||
_MAX_ANNOUNCE,
|
||||
_channels,
|
||||
_compact_num,
|
||||
_delete,
|
||||
_derive_name,
|
||||
_errors,
|
||||
@@ -44,26 +45,48 @@ from plugins.youtube import ( # noqa: E402
|
||||
YT_ATOM_FEED = b"""\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns:yt="http://www.youtube.com/xml/schemas/2015"
|
||||
xmlns="http://www.w3.org/2005/Atom">
|
||||
xmlns="http://www.w3.org/2005/Atom"
|
||||
xmlns:media="http://search.yahoo.com/mrss/">
|
||||
<title>3Blue1Brown - Videos</title>
|
||||
<author><name>3Blue1Brown</name></author>
|
||||
<entry>
|
||||
<id>yt:video:abc123</id>
|
||||
<yt:videoId>abc123</yt:videoId>
|
||||
<title>Linear Algebra</title>
|
||||
<published>2026-01-15T12:00:00+00:00</published>
|
||||
<link rel="alternate" href="https://www.youtube.com/watch?v=abc123"/>
|
||||
<media:group>
|
||||
<media:community>
|
||||
<media:statistics views="1500000"/>
|
||||
<media:starRating count="45000"/>
|
||||
</media:community>
|
||||
</media:group>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>yt:video:def456</id>
|
||||
<yt:videoId>def456</yt:videoId>
|
||||
<title>Calculus</title>
|
||||
<published>2026-02-01T08:30:00+00:00</published>
|
||||
<link rel="alternate" href="https://www.youtube.com/watch?v=def456"/>
|
||||
<media:group>
|
||||
<media:community>
|
||||
<media:statistics views="820000"/>
|
||||
<media:starRating count="32000"/>
|
||||
</media:community>
|
||||
</media:group>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>yt:video:ghi789</id>
|
||||
<yt:videoId>ghi789</yt:videoId>
|
||||
<title>Neural Networks</title>
|
||||
<published>2026-02-10T14:00:00+00:00</published>
|
||||
<link rel="alternate" href="https://www.youtube.com/watch?v=ghi789"/>
|
||||
<media:group>
|
||||
<media:community>
|
||||
<media:statistics views="250000"/>
|
||||
<media:starRating count="12000"/>
|
||||
</media:community>
|
||||
</media:group>
|
||||
</entry>
|
||||
</feed>
|
||||
"""
|
||||
@@ -362,6 +385,28 @@ class TestParseFeed:
|
||||
channel_name, _ = _parse_feed(YT_ATOM_FEED)
|
||||
assert channel_name == "3Blue1Brown"
|
||||
|
||||
def test_parses_published_date(self):
|
||||
_, items = _parse_feed(YT_ATOM_FEED)
|
||||
assert items[0]["date"] == "2026-01-15"
|
||||
assert items[1]["date"] == "2026-02-01"
|
||||
assert items[2]["date"] == "2026-02-10"
|
||||
|
||||
def test_parses_views(self):
|
||||
_, items = _parse_feed(YT_ATOM_FEED)
|
||||
assert items[0]["views"] == 1500000
|
||||
assert items[1]["views"] == 820000
|
||||
|
||||
def test_parses_likes(self):
|
||||
_, items = _parse_feed(YT_ATOM_FEED)
|
||||
assert items[0]["likes"] == 45000
|
||||
assert items[1]["likes"] == 32000
|
||||
|
||||
def test_no_media_defaults_zero(self):
|
||||
_, items = _parse_feed(YT_ATOM_NO_VIDEOID)
|
||||
assert items[0]["views"] == 0
|
||||
assert items[0]["likes"] == 0
|
||||
assert items[0]["date"] == ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestResolveChannel
|
||||
@@ -789,6 +834,11 @@ class TestCmdYtCheck:
|
||||
assert len(announcements) == 2
|
||||
assert "[news]" in announcements[0]
|
||||
assert "Calculus" in announcements[0]
|
||||
# Verify metadata suffix (views, likes, date)
|
||||
assert "| " in announcements[0]
|
||||
assert "820kv" in announcements[0]
|
||||
assert "32klk" in announcements[0]
|
||||
assert "2026-02-01" in announcements[0]
|
||||
|
||||
asyncio.run(inner())
|
||||
|
||||
@@ -1103,3 +1153,27 @@ class TestCmdYtUsage:
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_yt(bot, _msg("!yt foobar")))
|
||||
assert "Usage:" in bot.replied[0]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestCompactNum
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCompactNum:
|
||||
def test_zero(self):
|
||||
assert _compact_num(0) == "0"
|
||||
|
||||
def test_small(self):
|
||||
assert _compact_num(999) == "999"
|
||||
|
||||
def test_one_k(self):
|
||||
assert _compact_num(1000) == "1k"
|
||||
|
||||
def test_fractional_k(self):
|
||||
assert _compact_num(1500) == "1.5k"
|
||||
|
||||
def test_one_m(self):
|
||||
assert _compact_num(1_000_000) == "1M"
|
||||
|
||||
def test_fractional_m(self):
|
||||
assert _compact_num(2_500_000) == "2.5M"
|
||||
|
||||
Reference in New Issue
Block a user