Draft
Conversation
- Add Weekly Roundup config section to dashboard (enable toggle, channel select, weekday, hour, embed color) - Wire WEEKLY_ROUNDUP_CHANNEL_ID into Discord channel loader - Bump package.json to 1.5.4 and add CHANGELOG entry
- Add utils/i18n.js: minimal Node-side loader that reads
locales/<LANGUAGE>.json with fallback to en.json, exports t(key, vars)
with dot-notation lookup and {placeholder} interpolation
- Add roundup.* namespace to en/de/sv/template locales
- Replace all user-visible strings in bot/weeklyRoundup.js with t() calls
(embed title, season/episode labels, footer, fallbacks)
- Logger messages remain in English by convention
Must-fix from code-review + silent-failure-hunter:
- Scheduler: now.getHours() === targetHour (was <, caused re-posts
throughout the target hour+)
- Failure counter scoped per-week with weekKey, resets automatically
on entering a new week (was process-global, permanently died after
3 lifetime fails)
- fetchRecentlyAdded throws on error instead of returning []; roundup
and poller wrap it properly so a Jellyfin outage no longer looks
like a quiet week
- Rename Jellyfin filter param: MinDateLastSaved -> MinDateCreated
(LastSaved changes on metadata refresh, wrong semantics)
- escapeMd now also escapes * _ ~ ` to prevent titles like *Batman*
from breaking bold formatting inside the link label
- sendWeeklyRoundup: channel-fetch failure now logs a warn, no more
silent return
- runTick wrapper catches rejected promises from setTimeout/setInterval
- markPosted: on persistence failure, do NOT set process.env so the
next tick retries (was in-memory-only success)
- resolveLibraryNames: propagates errors to caller instead of silently
returning {} (caller now fails the tick and bumps failure count)
- parseIntInRange for WEEKDAY/HOUR rejects NaN/out-of-range values
- i18n: whitelist LANGUAGE env var against ^[a-zA-Z]{2,3}([_-]...)?$
so a malicious or typoed value cannot point fs.readFileSync at an
arbitrary path
- formatDate: normalize LANGUAGE to primary BCP-47 subtag before
passing to Intl
Also: buildJellyfinUrl: log fallback at error level so malformed
JELLYFIN_BASE_URL is loud in ops logs.
If the Discord post succeeded but writing lastPostedAt to config failed, the previous logic bumped the weekly failure counter. After 3 of those the whole week was skipped despite users having already seen the post. Set the in-memory env var unconditionally so the same process won't re-post, and log the persistence failure without touching the counter.
- resolveLibraryNames: catch fetchLibraries errors and fall back to the generic library label instead of letting a names-only blip nuke the whole weekly post - formatDate: log the Intl failure before falling back to ISO date - buildJellyfinUrl: ensure the concat fallback is a syntactically valid URL (prefix with http://invalid.local/ sentinel when JELLYFIN_BASE_URL lacks a scheme) so downstream ButtonBuilder.setURL surfaces the misconfig clearly rather than with an opaque validation error
The weekly_roundup_* and weekday_* data-i18n keys were present in the HTML but never added to the locale files. Users on non-English locales saw raw keys like 'config.weekly_roundup_title' instead of translated labels. Added all keys to en, de, sv, and template.
Mirrors the existing 'Test Random Pick' pattern — posts the weekly roundup immediately without waiting for the scheduled weekday/hour. sendWeeklyRoundup gains an options.test flag. In test mode it rethrows errors (so the HTTP handler can surface them) and skips all state side effects (lastPostedAt, failure counter) so a test run never masks or replaces the real scheduled post.
Log the raw Jellyfin item count and the post-library-filter count on every fetch so the actual filtering stage is visible. In test mode, distinguish three failure cases when nothing would be posted: - no notification libraries configured at all - Jellyfin returned items but the library filter dropped all of them - Jellyfin returned nothing in the 7-day window The scheduled path is unchanged — a library mismatch is still a normal quiet week for that user, not a failure to count.
Jellyfin libraries have a CollectionId (referenced by Item.ParentId / AncestorIds) and a separate VirtualFolderItemId (stored in JELLYFIN_NOTIFICATION_LIBRARIES via the dashboard). Comparing AncestorIds directly against the config keys never matched, so the roundup filtered every item out even when Jellyfin returned 200. fetchWindowItems now calls fetchLibraryMap() and runs every candidate ID through resolveConfigLibraryId() before checking membership — the same translation the webhook flow already does. The resolved id is stashed on each item as _configLibraryId so groupItems can reuse it without redoing the lookup.
When Jellyfin returns items but every one is filtered out, dump the configured library ids, the known Jellyfin library ids in both forms, and the first item's ParentId/AncestorIds (raw + translated) so we can see exactly where the comparison diverges.
…ering The previous approach fetched the 200 most recent Jellyfin items globally and then filtered by AncestorIds. That breaks when items live in libraries that /Library/VirtualFolders doesn't return (BoxSets, Collections, unmapped folders) — every item gets dropped even though they're really inside a configured library. Now we issue one /Items query per configured library id with ParentId + Recursive, which delegates membership resolution to Jellyfin itself. No more two-form-id dance, no more ancestor walks. Also defensively skip non-hex-32 entries in JELLYFIN_NOTIFICATION_LIBRARIES — observed an 'on' string leaking in from somewhere upstream, will need a separate fix for the source.
Sonarr quality upgrades delete and re-import the same episode file,
which Jellyfin can register as multiple new items with the same
SeriesId + ParentIndexNumber + IndexNumber within the same week. The
previous counter would then report '3 new episodes' for what was
actually the same episode imported three times.
seasons is now Map<seasonNum, Set<episodeKey>>, where episodeKey is
'e{IndexNumber}' for normal episodes and falls back to the Jellyfin
item id for unnumbered specials (which keeps per-item dedup but lets
specials still appear).
Episode dedup key now prefers, in order: IndexNumber + IndexNumberEnd (2-parters), IndexNumber alone, lowercased Name (Sonarr re-imports keep the title), then item id as last resort. The previous version fell straight to item id when IndexNumber was missing, which made every re-import look like a different episode. Also log the raw identity fields for the first 30 episodes per run so we can see exactly what Jellyfin returns and confirm where dedup breaks if a case still slips through.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional Weekly Roundup that posts a weekly digest of new Jellyfin content to a Discord channel. Disabled by default; configurable via the dashboard.
Changes
bot/weeklyRoundup.js(new): hourly scheduler tick, 7-day rolling window fetch from Jellyfin, per-library grouping, series/season/episode collapsing (e.g. "My Show — Seasons 1 & 2 (12 episodes)"), embed builder with Jellyfin deeplinks, 3-strike back-off on consecutive failuresbot/botManager.js: wiresscheduleWeeklyRoundup(client)into theclientReadyhandler next to the daily random picklib/config.js,utils/validation.js): addsWEEKLY_ROUNDUP_{ENABLED,CHANNEL_ID,WEEKDAY,HOUR,EMBED_COLOR,LAST_POSTED_AT}keys and Joi validatorsweb/index.html,web/script.js): new "Weekly Roundup" section with enable toggle, channel select, weekday dropdown, hour input, embed color;WEEKLY_ROUNDUP_CHANNEL_IDis populated via the existing Discord channel loaderutils/i18n.js(new): minimal server-side i18n loader. Readslocales/<LANGUAGE>.jsonwith fallback toen.json; exportst(key, vars)with dot-notation lookup and{placeholder}interpolationlocales/{en,de,sv,template}.json): newroundup.*namespace covering embed title, season/episode labels, footer, and fallbacks — all user-visible strings in the roundup go throught()utils/jellyfinUrl.jsextracted fromjellyfinWebhook.jsso the roundup can build Jellyfin deeplinks without duplicating logic;api/jellyfin.js::fetchRecentlyAddedgained an optionalminDateCreatedparam (maps to Jellyfin'sMinDateLastSaved) to support the rolling 7-day windowIdempotency
The scheduler ticks every hour and gates on a persisted
WEEKLY_ROUNDUP_LAST_POSTED_ATtimestamp, so:ALREADY_POSTED_MIN_AGE_MSis 6 days (not 7) to tolerate small scheduler driftVersion
Bumps
package.jsonto1.5.4and adds aCHANGELOG.mdentry.