# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- ## [1.5.5] - 2026-06-14 ### 🌍 Added - **French translation (`fr`)**: Anchorr is now fully translated into French, covering all bot messages and UI text. ### πŸ› Fixed - **Autocomplete timeouts on movie/show search**: Per-item TMDB detail lookups during autocomplete could push the response past Discord's 3-second interaction window, causing the suggestion list to fail to appear. These per-item lookups are now skipped during autocomplete. --- ## [1.5.4] - 2026-04-26 ### πŸ› Fixed - **Sonarr/Radarr quality upgrades no longer trigger duplicate "new content" Discord notifications.** When Sonarr or Radarr replaced an existing file with a higher-quality release, Jellyfin assigned a new internal `ItemId` to the re-imported file. Anchorr's dedup keyed off that ItemId and treated the upgrade as a brand-new item. The dedup layer now keys items by a stable identity (TMDB ID + season/episode for shows, TMDB ID for movies) that survives file replacement. Dedup window extended to 7 days to cover typical upgrade cycles. - **Dedup state survives container restarts.** Previously the in-memory dedup maps were wiped on every restart, so the next poll/WebSocket reconnect would re-notify everything in the recently-added window. State is now persisted to `config/dedup-*.json` and reloaded on startup, with expired entries dropped automatically. --- ## [1.5.3] - 2026-04-20 ### πŸ”’ Security - **Pin `follow-redirects` to `^1.16.0`** (GHSA-r4q5-vmmm-2653): Versions `<= 1.15.11` only strip `authorization`, `proxy-authorization`, and `cookie` headers on cross-domain redirects, so custom auth headers (`X-Api-Key` for Jellyseerr, `X-MediaBrowser-Token` for Jellyfin) would have leaked to the redirect target. Exploitability in Anchorr is low β€” the affected hosts are user-configured self-hosted services β€” but the patched version strips custom auth headers on cross-domain redirect by default. Pinned via `overrides` in `package.json` since `follow-redirects` is a transitive dependency of `axios`. ### πŸ› Fixed - **Missing backdrop image on single-episode Discord notifications**: When Jellyfin sent an episode webhook, the TMDB lookup used the episode's own TMDB ID (not the series'), which typically returned no data. The fallback then tried to load a backdrop from the episode item in Jellyfin β€” but episodes don't carry their own backdrop image, only the parent series does. The notification was rendered with a blank image. Episode notifications now fall back to the parent series' backdrop (`Items/{SeriesId}/Images/Backdrop`) when TMDB data isn't available, matching the visual consistency of movie and series notifications. --- ## [1.5.2] - 2026-04-17 ### πŸ› Fixed - **Dashboard save broken for library notification mapping**: Saving the Jellyfin library-to-channel mapping failed with `does not match any of the allowed types` after the per-library anime toggle was added. The Joi validator only accepted the legacy `{ libraryId: channelId }` shape and rejected the current `{ libraryId: { channel, isAnime } }` shape, so the mapping never persisted and per-library notifications fell back to the default channel - **Config startup crash**: A corrupted or malformed `USER_MAPPINGS` value in `config.json` no longer crashes the app on startup β€” the migration step is now skipped with a clear error log instead - **Config volume detection**: When `/config` exists but is not writable, a warning is now logged showing the exact reason (`EACCES`, `EROFS`, etc.) so Docker users understand why the fallback path is used - **Bot start failure HTTP status**: The `/api/config` endpoint now returns HTTP 500 (instead of 200) when the bot fails to auto-start after config save, allowing the dashboard to correctly surface the error - **ROLE_ALLOWLIST / ROLE_BLOCKLIST parse failure**: A malformed role list in config now logs at `error` level (previously `warn`) with an explicit note that enforcement is disabled β€” avoids silently granting access to all users - **Discord command registration failure**: When global command registration fails and no `GUILD_ID` is set, the error is now logged at `error` level with the failure reason, making it clear that slash commands may not appear - **Pending requests corruption**: If the pending-requests state file on disk is unreadable, the corrupted file is renamed to a timestamped backup before starting fresh β€” prevents repeated parse failures on restart - **TMDB strategy failures**: Failed random-pick strategies, detail enrichment, and trending fetch now log at `warn` level instead of failing silently β€” operators can see when TMDB is returning errors (rate limit, bad key, etc.) - **Log route fallback**: When the log directory cannot be enumerated (e.g. permission issue), the fallback to the default log path is now logged as a warning instead of silently applied - **deferReply failure feedback**: When deferring a Discord interaction fails, the bot now attempts a direct ephemeral reply so the user sees an error message instead of Discord's generic "interaction failed" screen ### 🌍 Internationalization - **Dashboard toast messages translated**: All previously hardcoded English status and error messages in the web dashboard are now routed through the i18n system. Affected messages include copy confirmations, mapping add/remove feedback, connection validation warnings, user lookup failures, auto-map/sync previews, bot control errors, and more β€” 22 strings in total - **New locale keys** added to all supported languages (en, de, sv) and the translation template: `errors.fetch_config`, `errors.autostart_check`, `errors.no_webhook_secret`, `errors.copy_url_failed`, `errors.jellyfin_url_required`, `errors.jellyfin_api_key_required`, `errors.mapping_refresh_failed`, `errors.discord_user_lookup_failed`, `errors.automap_preview_failed`, `errors.sync_preview_failed`, `errors.refresh_users_failed`, `errors.bot_control_failed`, `errors.remove_mapping_failed`, `errors.save_mappings_failed`, `errors.remove_mappings_failed`, `errors.language_load_failed`, `success.webhook_secret_copied`, `success.webhook_url_copied`, `user_mapping.mapping_removed`, `user_mapping.mapping_added`, `user_mapping.no_mappings_selected`, `user_mapping.select_users` --- ## [1.5.1] - 2026-04-16 ### πŸ”’ Security - **Fix credential exfiltration via SSRF in config-test endpoints** (GHSA-ph98-5xm3-37w3): The `POST /test-seerr`, `/seerr/quality-profiles`, `/seerr/servers`, and `POST /jellyfin-libraries` endpoints would forward the real stored API key to any URL supplied by an authenticated user. The real key is now only substituted when the submitted URL's host matches the currently configured server host β€” if they differ, the request is rejected with 403. Thanks to [@whoopsi-daisy](https://github.com/whoopsi-daisy) for the responsible disclosure. --- ## [1.5.0] - 2026-04-14 ### ✨ Added - **Anime quality profiles & server selection**: Separate default quality profiles and Radarr/Sonarr servers can now be configured for anime content (both TV series and movies). Anime is detected automatically via TMDB metadata (Animation genre + Japanese origin). If no anime-specific config is set, the standard movie/TV defaults are used β€” existing setups are unaffected - **Jellyseerr `isAnime` flag**: When anime content is detected, the `isAnime: true` flag is included in the Jellyseerr request payload, allowing Jellyseerr to route to its anime-configured instance - **Anime library toggle**: Each library row in the Jellyfin notification mapping UI now has a compact checkbox to manually mark it as an anime library. This is explicit and works regardless of library naming. When flagged, the `isAnime` flag is passed through to all notification paths (webhook, poller, WebSocket) β€” independent of TMDB metadata detection - **CI: npm audit gate**: A new `audit` job runs `npm audit --audit-level=high` before the Docker build β€” vulnerable dependencies now block the pipeline ### πŸ› Fixed - **Jellyfin library matching**: Libraries with `null` or `mixed` CollectionType no longer get incorrectly skipped during notification path matching - **Docker config volume permissions**: Added entrypoint script to fix ownership on first run, preventing write failures for the non-root container user - **Seerr error messages**: Request failures now surface specific messages (auth errors, server errors, connection refused) instead of generic "An error occurred" - **Seerr `checkMediaStatus` error handling**: Network and 5xx errors are now propagated instead of silently returning `{ exists: false }` - **Silent failures in dashboard**: Previously empty or unchecked `catch` blocks in the web dashboard now log errors at the appropriate level (`console.error`/`console.warn`/`console.debug`). Affected paths include bot status polling, logout, guild/channel loading, role loading, connection status checks, webhook secret loading, and localStorage cache access - **Legacy library config migration**: The `JELLYFIN_NOTIFICATION_LIBRARIES` config is now parsed consistently across all notification paths. Legacy array format (`["libId1", "libId2"]`) and legacy string-value format (`{ libId: "channelId" }`) are both migrated transparently to the new object format (`{ libId: { channel, isAnime } }`) at read time ### πŸ—οΈ Code Quality - **Shared library resolver**: All three notification sources (webhook, poller, WebSocket) now go through shared `getLibraryChannels()`, `resolveTargetChannel()`, and `getLibraryAnimeFlag()` functions in `jellyfin/libraryResolver.js`, eliminating duplicated inline logic ### πŸ—‘οΈ Removed - **Legacy `.env` migration**: The automatic `.env` β†’ `config.json` migration has been removed. If a `.env` file is detected, a warning is logged pointing to the web dashboard --- ## [1.4.9] - 2026-04-03 ### πŸ”’ Security - **Content-Security-Policy header added**: The dashboard now sends a `Content-Security-Policy` header restricting scripts to `'self'` and `cdn.jsdelivr.net`, styles to `'self'`, `cdnjs.cloudflare.com`, and `cdn.jsdelivr.net`, and blocking all plugins (`object-src 'none'`). Inline `