From 9dd04067bb8e165039ef74a916b1d018203b8cbb Mon Sep 17 00:00:00 2001 From: Maddox Date: Sun, 3 May 2026 10:21:59 -0400 Subject: [PATCH] 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. --- maubot.yaml | 2 +- media_bot/bot.py | 99 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/maubot.yaml b/maubot.yaml index 9f3a7ba..8423e20 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -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 diff --git a/media_bot/bot.py b/media_bot/bot.py index 9ca0b25..fce9c45 100644 --- a/media_bot/bot.py +++ b/media_bot/bot.py @@ -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."""