v0.5.2: digest skips redundant/empty sections, splits long bodies

Daily-digest fixes for issue #1:
- Drop the Completed section unless a finished release isn't already
  represented in Recently Added (slug-substring match on titles).
- Skip the Queued section when both Sonarr and Radarr queues are empty.
- Split the message into numbered chunks (~3000 chars) so Sunday digests
  with the emby-cleaner recap don't get truncated by Matrix clients.
This commit is contained in:
Maddox 2026-05-03 10:21:59 -04:00
parent c62bb9c03e
commit b23eb8b403
2 changed files with 83 additions and 18 deletions

View file

@ -1,6 +1,6 @@
maubot: 0.3.1
id: com.3ddbrewery.media
version: 0.5.1
version: 0.5.2
license: MIT
modules:
- media_bot

View file

@ -1422,7 +1422,13 @@ class MediaBot(Plugin):
self.log.exception("DB schema bootstrap failed")
async def _send_digest(self, room: str) -> None:
"""Build and post the daily digest to `room`."""
"""Build and post the daily digest to `room`.
Skips the Completed section entirely when every finished release is
already represented in Recently Added; skips Queued when both arrs are
empty; splits long bodies (e.g. Sunday cleanup recap) across numbered
messages so Matrix clients don't truncate.
"""
cutoff = datetime.now(timezone.utc).timestamp() - 86400
emby_uid = self._any_emby_uid("")
added: list[dict] = []
@ -1449,6 +1455,19 @@ class MediaBot(Plugin):
s_q = sonarr_q if not isinstance(sonarr_q, Exception) else []
r_q = radarr_q if not isinstance(radarr_q, Exception) else []
added_titles = {self._title_slug(self._emby_match_title(it)) for it in added}
added_titles.discard("")
def _is_unique(release_name: str) -> bool:
slug = self._title_slug(release_name)
if not slug:
return True
return not any(t and t in slug for t in added_titles)
nzb_unique = [h for h in nzb_recent
if _is_unique(h.get("Name") or h.get("NZBName") or "")]
qbt_unique = [t for t in qbt_recent if _is_unique(t.get("name") or "")]
local = _local_now()
is_sunday = local.weekday() == 6 # Mon=0..Sun=6
today_str = local.strftime("%A, %b %d")
@ -1458,18 +1477,24 @@ class MediaBot(Plugin):
lines.append("- " + self._fmt_emby_item(it))
if not added:
lines.append("- (nothing in the last sweep)")
lines.append("")
total_completed = len(nzb_recent) + len(qbt_recent)
lines.append(f"**✅ Completed last 24h: {total_completed}** "
f"(NZBGet: {len(nzb_recent)} · qBt: {len(qbt_recent)})")
for h in nzb_recent[:5]:
name = h.get("Name") or h.get("NZBName") or "?"
size_mb = h.get("FileSizeMB") or 0
lines.append(f"- *{name}* — {size_mb / 1024:.1f} GB")
for t in qbt_recent[:5]:
lines.append(f"- *{t.get('name','?')}* — {_human_bytes(t.get('size') or 0)}")
lines.append("")
lines.append(f"**📥 Queued: {len(s_q)} TV · {len(r_q)} movies**")
if nzb_unique or qbt_unique:
lines.append("")
lines.append(
f"**✅ Also completed (not yet in Emby): "
f"{len(nzb_unique) + len(qbt_unique)}** "
f"(NZBGet: {len(nzb_unique)} · qBt: {len(qbt_unique)})"
)
for h in nzb_unique[:5]:
name = h.get("Name") or h.get("NZBName") or "?"
size_mb = h.get("FileSizeMB") or 0
lines.append(f"- *{name}* — {size_mb / 1024:.1f} GB")
for t in qbt_unique[:5]:
lines.append(f"- *{t.get('name','?')}* — {_human_bytes(t.get('size') or 0)}")
if s_q or r_q:
lines.append("")
lines.append(f"**📥 Queued: {len(s_q)} TV · {len(r_q)} movies**")
if is_sunday:
cleaner_lines = await self._fetch_emby_cleaner_recap()
@ -1478,10 +1503,50 @@ class MediaBot(Plugin):
lines.append("**🧹 Weekly cleanup (emby-cleaner):**")
lines.extend(cleaner_lines)
await self.client.send_message(
RoomID(room),
TextMessageEventContent(msgtype=MessageType.NOTICE, body="\n".join(lines)),
)
chunks = self._chunk_lines(lines)
total = len(chunks)
for idx, chunk in enumerate(chunks, 1):
body = chunk if total == 1 else f"**({idx}/{total})**\n\n{chunk}"
await self.client.send_message(
RoomID(room),
TextMessageEventContent(msgtype=MessageType.NOTICE, body=body),
)
@staticmethod
def _emby_match_title(it: dict) -> str:
"""Pick the title that should match a release name — series name for
episodes, item name otherwise."""
if it.get("Type") == "Episode":
return it.get("SeriesName") or it.get("Name") or ""
return it.get("Name") or ""
@staticmethod
def _title_slug(s: str) -> str:
"""Normalize a title or release name to lowercase alphanumerics so
'The.Bear.S03E10.1080p...' and 'The Bear' compare equal-ish."""
return "".join(c for c in s.lower() if c.isalnum())
@staticmethod
def _chunk_lines(lines: list[str], max_len: int = 3000) -> list[str]:
"""Pack lines into chunks <= max_len chars on line boundaries."""
body = "\n".join(lines)
if len(body) <= max_len:
return [body]
chunks: list[str] = []
cur: list[str] = []
cur_len = 0
for line in lines:
extra = len(line) + (1 if cur else 0)
if cur and cur_len + extra > max_len:
chunks.append("\n".join(cur))
cur = [line]
cur_len = len(line)
else:
cur.append(line)
cur_len += extra
if cur:
chunks.append("\n".join(cur))
return chunks
async def _fetch_emby_cleaner_recap(self) -> list[str]:
"""Pull the last 12h of messages from the emby-cleaner ntfy topic."""