feat(notify): value-first emoji rows
ci / docker (push) Successful in 24s
test / komodo-deploy-script (push) Successful in 26s
test / komodo-deploy-script (pull_request) Successful in 16s

Switch the summary to the layout jblu picked: each counter renders as
"emoji value label", grouped two per line and joined by a middle dot.

    Scanned 42 torrents in 12s

    🗑️ 7 removed  ·  👻 4 orphaned
    ⏭️ 5 ignored  ·  🐣 3 premature

    🛡️ 10 protected  ·  🌐 20 public

- the value leads, so the numbers are the first thing the eye lands on
- html style now bolds the values instead of the labels
- "Non-protected" reads as "public", matching the non_protected_tag
- use_emoji now defaults to true; false gives the plain variant
- dry runs read "7 would be removed"
This commit is contained in:
trex Bot committed 2026-11-03 15:10:03 -06:00
1 parent 5d9da0d22b
commit dfce115fd8
5 files changed
+90 -51

No files matched your search

+49 -19
View File
@@ -190,16 +190,28 @@ class TestQbitmaid(unittest.TestCase):
self.assertEqual(stats["runtime"], "unknown")
def test_build_summary_plain(self):
title, body = build_summary(FakeQbt(), style="plain")
title, body = build_summary(FakeQbt(), style="plain", use_emoji=False)
self.assertEqual(title, "qbit-maid: 7 torrents removed")
self.assertEqual(
body,
"Scanned 42 torrents in 12s\n"
"\n"
"Removed 7 \u00b7 Orphaned 4\n"
"Ignored 5 \u00b7 Premature 3\n"
"7 removed \u00b7 4 orphaned\n"
"5 ignored \u00b7 3 premature\n"
"\n"
"Protected 10 \u00b7 Non-protected 20",
"10 protected \u00b7 20 public",
)
def test_build_summary_plain_emoji(self):
_, body = build_summary(FakeQbt(), style="plain", use_emoji=True)
self.assertEqual(
body,
"Scanned 42 torrents in 12s\n"
"\n"
"\U0001F5D1\uFE0F 7 removed \u00b7 \U0001F47B 4 orphaned\n"
"\u23ED\uFE0F 5 ignored \u00b7 \U0001F423 3 premature\n"
"\n"
"\U0001F6E1\uFE0F 10 protected \u00b7 \U0001F310 20 public",
)
def test_build_summary_plain_has_no_leading_indent(self):
@@ -224,33 +236,47 @@ class TestQbitmaid(unittest.TestCase):
for row in rows:
self.assertEqual(row.count("\u00b7"), 1, row)
def test_build_summary_value_leads_label(self):
"""The number comes first so it is the first thing the eye lands on."""
_, body = build_summary(FakeQbt(), style="plain", use_emoji=False)
self.assertIn("7 removed", body)
self.assertIn("20 public", body)
self.assertNotIn("Removed 7", body)
self.assertNotIn("Non-protected", body)
def test_build_summary_html(self):
title, body = build_summary(FakeQbt(), style="html")
self.assertEqual(title, "qbit-maid: 7 torrents removed")
self.assertIn("Scanned <b>42</b> torrents in <b>12s</b>", body)
self.assertIn("<b>Removed</b> 7", body)
self.assertIn("<b>Non-protected</b> 20", body)
self.assertIn("<b>7</b> removed", body)
self.assertIn("<b>20</b> public", body)
self.assertNotIn("---", title)
def test_build_summary_emoji(self):
_, plain = build_summary(FakeQbt(), style="plain", use_emoji=True)
self.assertIn("\U0001F5D1\uFE0F Removed 7", plain)
self.assertIn("\U0001F47B Orphaned 4", plain)
self.assertIn("\U0001F5D1\uFE0F 7 removed", plain)
self.assertIn("\U0001F47B 4 orphaned", plain)
_, html = build_summary(FakeQbt(), style="html", use_emoji=True)
self.assertIn("\U0001F5D1\uFE0F <b>Removed</b> 7", html)
self.assertIn("\U0001F5D1\uFE0F <b>7</b> removed", html)
def test_build_summary_emoji_off_has_no_emoji(self):
_, body = build_summary(FakeQbt(), style="plain", use_emoji=False)
for marker in ("\U0001F5D1", "\U0001F47B", "\u23ED", "\U0001F423",
"\U0001F6E1", "\U0001F310"):
self.assertNotIn(marker, body)
def test_build_summary_delete_disabled(self):
title, body = build_summary(FakeQbt(delete_torrents=False))
self.assertEqual(title, "qbit-maid: 7 torrents would be removed")
self.assertIn("Would remove 7", body)
self.assertNotIn("Removed", body)
def test_build_summary_does_not_repeat_title_in_body(self):
"""The title already states the outcome; the body must not restate it."""
title, body = build_summary(FakeQbt(), style="plain")
self.assertIn("7 would be removed", body)
self.assertNotIn("7 removed", body)
def test_build_summary_body_does_not_repeat_title_phrase(self):
"""The title carries the outcome; the body must not restate it verbatim."""
title, body = build_summary(FakeQbt(), style="plain", use_emoji=False)
self.assertNotIn("torrents removed", body)
self.assertIn("Removed 7", body)
self.assertNotIn("torrents would be removed", body)
self.assertIn("7 removed", body)
self.assertTrue(title.endswith("7 torrents removed"))
def test_build_summary_nothing_to_do(self):
@@ -276,16 +302,20 @@ class TestQbitmaid(unittest.TestCase):
def test_get_notify_options_defaults(self):
opts = get_notify_options(FakeQbt())
self.assertEqual(opts["style"], "plain")
self.assertFalse(opts["use_emoji"])
self.assertTrue(opts["use_emoji"])
self.assertFalse(opts["include_hostname"])
self.assertTrue(opts["notify_on_empty"])
self.assertIsNone(opts["priority"])
self.assertIsNone(opts["sound"])
def test_get_notify_options_emoji_can_be_disabled(self):
opts = get_notify_options(FakeQbt(config={"pushover": {"use_emoji": False}}))
self.assertFalse(opts["use_emoji"])
def test_get_notify_options_overrides(self):
fake = FakeQbt(config={"pushover": {
"format": "HTML",
"use_emoji": True,
"use_emoji": False,
"include_hostname": True,
"notify_on_empty": False,
"priority": 2,
@@ -295,7 +325,7 @@ class TestQbitmaid(unittest.TestCase):
}})
opts = get_notify_options(fake)
self.assertEqual(opts["style"], "html")
self.assertTrue(opts["use_emoji"])
self.assertFalse(opts["use_emoji"])
self.assertTrue(opts["include_hostname"])
self.assertFalse(opts["notify_on_empty"])
self.assertEqual(opts["priority"], 2)