github joecarl/mularr v0.18.0

3 hours ago

What's Changed

New features

  • Webhook extensions. A new webhook extension type sends a JSON POST to a URL of your choice whenever the selected events happen: download.added, download.completed, download.cancelled, search.started, blacklist.added and system.alert. Payload is { event, timestamp, data }; calls run in parallel with a 10 s timeout and failures are only logged. Configure it from Extensions.
  • Modern theme. A new flat, light theme for those who don't feel comfortable with retro.
  • Sessions survive restarts even if JWT_SECRET is not set. Mularr now generates a secret once and stores it in a jwt-secret file next to the database, so you no longer get logged out every time the container restarts.
  • UMASK support. Set UMASK=002 (or any octal mask) to make downloads, config and database group-writable, so Sonarr/Radarr running under another UID can move, delete or hardlink the files.
  • GLUETUN_PORT_INDEX. When Gluetun forwards several ports, pick which entry of the ports array aMule should use. Leave it unset to keep the current behaviour.
  • LOG_LEVEL. All services now log through a single logger with debug | info | warn | error levels. debug also logs every Sonarr/Radarr request, search progress and full Torznab responses.

Security

  • Username/password checks use constant-time comparison, so response timing can no longer reveal which credential is wrong.
  • The WebSocket endpoint now requires authentication. The web UI passes its session token automatically; custom clients must send ?token=<jwt|apikey> (browsers cannot set headers on a WebSocket). Unauthenticated sockets are closed with code 4401.
  • The qBittorrent app/preferences endpoint now sits behind authentication, since it exposes the save/temp paths on disk. app/version and app/webapiVersion stay public so the *arr probe still works before login.
  • The SID cookie is now SameSite=Strict.
  • Unauthorized requests no longer dump request headers and query string to the log (they contained the tokens and API keys themselves). The log now only lists which kind of credential was presented.

Improvements

  • Telegram indexer: search results are re-checked against Telegram and media that has been deleted or edited away is purged from the index, so *arr apps stop grabbing files that can no longer be downloaded. Confirmed hits are trusted for 6 hours, and search pages grew from 20 to 100 messages to match Telegram's batch size. Starting a download of a vanished message also removes it from the index.
  • aMule daemon lifecycle: start, restart and config rewrites are now serialized. A Gluetun port change and the monitor detecting a stopped daemon can no longer race and spawn two amuled instances or lose the new config. Settings are validated before the daemon is stopped, so an invalid request leaves it running.
  • Public IP lookups are cached for 5 minutes (and IP details per address), so WebSocket reconnects and REST calls stop hammering ipify/ipinfo.
  • Extensions: the URL of an existing extension can now be edited without deleting and recreating it.
  • Search view shows the number of results.
  • Login screen shows the running Mularr version.
  • UI: consistent layout and spacing across Transfers, Search, Shared, Servers, Categories and Extensions; toolbar separators; new icon set.
  • qBittorrent API: creating a category with an empty name returns 400 instead of failing silently, and errors are reported with a proper 500.
  • Configuration is validated at startup. Every environment variable is read once; an invalid value (e.g. a non-integer PORT, an unknown LOG_LEVEL) fails fast with a clear message instead of surfacing as NaN or a silent default. Empty values are treated as unset, matching docker-compose.example.yml.
  • Gluetun notifications now say that aMule was restarted with the new port in a single message.

For developers

  • MOCK_MODE=true runs the backend against generated data (aMule, Gluetun, Telegram, public IP and speed history are all mocked) for UI work and screenshots. The mock database is created and reseeded in a throwaway temp directory on every start and bot notifications are disabled, so it can never touch a real system.
  • Wire-contract types now live in backend/src/types and are imported directly by the frontend, removing the duplicated definitions.
  • Frontend service resolution moved to Chispa's inject(); backend services resolve their dependencies as readonly fields.
  • Dockerfile uses npm ci for reproducible builds.
  • Updated dependencies: Chispa, better-sqlite3, node-telegram-bot-api, TypeScript 7.

Breaking changes

  • GET /api/v2/app/preferences (qBittorrent API) now requires authentication. Sonarr/Radarr already authenticate, so no action is needed for them.
  • The /ws WebSocket requires a token. Only affects custom clients; the web UI handles it.
  • The legacy POST /api/amule/download and POST /api/amule/download/command endpoints were removed. Use the media API the UI already relies on.
  • Invalid environment values now abort startup instead of being ignored.

Don't miss a new mularr release

NewReleases is sending notifications on new releases.