Repository navigation
Releases
v2.1.1 - Jellyfin 12 Fix
Jellyfin 12 support. Jellyfin 12 rejects the legacy X-Emby-Token header, so MediaWatch 2.1.0 and older logged Jellyfin rejected the configured JELLYFIN_API_KEY (HTTP 401) even with a valid key and showed the server offline. The bot now sends the key as Authorization: MediaBrowser Token="…", which Jellyfin 10.10, 10.11 and 12 all accept. Plex is not affected.
Upgrading
Change the tag to 2.1.1, then docker compose pull && docker compose up -d. No config change is needed, and your existing JELLYFIN_API_KEY keeps working.
2.1.1 (2026-11-11)
Fixed
v2.1.0 - Maintenance mode
Maintenance mode. /maintenance start swaps the red "Offline" dashboard for a yellow "Server is under maintenance 🛠️" embed with your reason, the expected end and whether the server answers right now. The bot presence shows 🛠️ Maintenance. With a duration it ends on its own once the time is up and the server is back; it survives a restart of the bot. Works the same for Plex and Jellyfin, and only DISCORD_AUTHORIZED_USERS can use it.
See Commands → /maintenance and The Dashboard → During maintenance.
Upgrading
Change the tag to 2.1.0, then docker compose pull && docker compose up -d. No config change is needed. The new options are optional and default to the current behaviour:
| Option | Default | |
|---|---|---|
maintenance.thumbnail_url |
"" |
Dashboard thumbnail during maintenance; empty uses dashboard.icon_url. The example config uses https://mediawatch.nichtlegacy.com/logo-maintenance.png. |
maintenance.auto_end |
true |
End maintenance on its own once the duration has passed and the server answers again. |
presence.maintenance_text |
"🛠️ Maintenance" |
Presence text during maintenance. |
presence.maintenance_status |
"idle" |
Presence status during maintenance. |
The /maintenance commands appear after the restart; if Discord does not show them yet, reload the client (Ctrl+R).
2.1.0 (2026-10-11)
Added
- dashboard: announce maintenance instead of showing the server offline (ec919b1)
- maintenance: add /maintenance start, end and status (ec919b1)
Fixed
- docker: keep the code read-only for the bot however it was checked out (53a3ed3)
Documentation
- add the GitHub social preview to the brand assets (55c4e3c)
v2.0.0 - MediaWatch
The project was called PlexWatch and only spoke to Plex. It is now
MediaWatch and monitors Plex or Jellyfin from the same bot.
⚠️ Breaking changes
Read the upgrade guide
before upgrading. In short:
- The Docker image moved. New path:
ghcr.io/nichtlegacy/mediawatch.
The oldghcr.io/nichtlegacy/plexwatchpackage keeps its 1.x tags and gets
no further updates, so an existing install will not be auto-upgraded into
2.0. You have to change the image path yourself. :latestnow follows themainbranch, every build that passes CI, not
only releases. Pin a version tag such as:2.0.0to stay on releases. A new
:devtag follows thedevbranch.DISCORD_GUILD_IDis now required. 1.x registered slash commands
globally; 2.0 registers them per guild. Without the variable the bot stops at
startup. On the first start with it set, leftover global commands from 1.x
are cleared so nothing appears twice.DISCORD_AUTHORIZED_USERSis now required and has to contain at least
one numeric user ID. In 1.x it was optional.config.jsonis replaced byconfig.yaml. An existing
data/config.jsonis converted automatically on first start. The original
file is left untouched and ignored from then on; the conversion is logged in
full. Only if the conversion fails does the bot stop, with manual
instructions.media_server.typeselects the platform (plexorjellyfin) and is
set toplexby the automatic conversion, which is what every 1.x install
was.presence.sectionsis replaced bypresence.libraries, a list of
library names. Display name and emoji now come from the library block. The
conversion translates the old list, and a hand-writtenconfig.yamlthat
still carries it is translated on load with a warning.data/user_mapping.jsonis rewritten in place into a platform-keyed
format that 1.x cannot read. Back it up if you might roll back.- Startup fails fast on a broken environment. Missing or unparsable
required variables abort the start with a message naming what is missing and
where to set it, instead of booting into a permanently "offline" dashboard. - A broken
config.yamlstops the bot instead of falling back to the
defaults, as 1.x did with a brokenconfig.json. With the new privacy
options, a silent fallback would reopen restricted views. The message names
the file, line and column./reload_configrejects such a file and keeps the
running configuration. An empty file still means defaults. - The dashboard turns offline after
server.offline_threshold(default
300seconds) instead of on the first failed check. Set it to0for the
1.x behaviour. - The container no longer runs the bot as root. It starts as root to adopt
the volume, then drops toPUID/PGID(default1000:1000). Hosts whose
share user differs, Unraid uses99:100, should set both, and recreate the
container after a change: a plaindocker restartkeeps the old values.
PUIDorPGID0is refused, and the container exits before the bot
starts. - A fresh Docker volume receives only
config.yaml.example. Copy it to
config.yaml; until then the bot runs on built-in defaults. 1.x copied a
complete default config into an empty volume. - A local run needs Python 3.12 (1.x: 3.8 and later). Recreate the
virtualenv and reinstallrequirements.txt. - The log file of a local run is now
logs/mediawatch_debug.log(was
logs/plexwatch_debug.log), and log lines carrymediawatch_botinstead of
plexwatch_bot. Update any log filter. /cogsis limited toDISCORD_AUTHORIZED_USERS, like the other cog
commands.- The single-module
cogs/plex_core.pyfrom 1.x is gone. The bot is now
cogs/media_core/, a package per platform on top of a shared layer, and
/load,/unloadand/reloadtakemedia_core, resolved to the active
platform, instead ofplex_core.
Added
- Jellyfin support: a full platform next to Plex, selected via
media_server.type, with the same dashboard, stream details, Kill Stream and
library statistics. Clients on the local network are recognised via
ipaddress, which covers Docker's default networks and IPv6, audio and
subtitle languages are shown as names (German, notdeu), and music
streams show bit depth and sample rate, as on Plex. - Multi-platform architecture:
cogs/media_core/with a shared layer
(models, formatters, config, dashboard embed, presence) and one package per
platform. - YAML configuration with a documented
data/config.yaml.example, and an
automatic conversion of a 1.xdata/config.json. A block whose options are
all commented out means the defaults, and an unknownmedia_server.type
falls back toplexwith a warning. - Environment validation at startup that checks the required Discord
variables and the credentials for the selected platform, rejects
non-numeric IDs, and reports the mismatch case explicitly
(media_server.typeis jellyfin but onlyPLEX_*is set). Partially
configured optional integrations warn instead of aborting. - Buttons under the dashboard: one
Stream N - Userbutton per running
stream, up to eight, and optionally🌐 Global Stats. They keep working
after a bot restart. - Stream details behind each stream button: device, player, quality,
transcode decision and tracks. On Plex they come from Tautulli, Jellyfin
serves them directly.TZsets the timezone of the start and end clock in
Plex stream details (default UTC). - Kill Stream for
DISCORD_AUTHORIZED_USERS, with a reason message
prefilled fromstream_controls.kill_stream.default_reason. On Plex it works
without Tautulli; on Jellyfin, which has no reason field, the reason is sent
as an on-screen message first. - Tautulli integration for Plex: user statistics in up to three pages
(watch behaviour, devices with a chart, top shows) and global server
statistics in three pages (overview, activity with a peak-hour chart, top 10
users by watch time). Global statistics are cached for five minutes and
shared between concurrent clicks. - Statistics options:
global_stats.button_locationputs the Global Stats
button in the stream details, on the dashboard, or both, and
global_stats.page_2.time_rangesets the window of the activity page.
user_stats.enabled,user_stats.pages,user_stats.time_rangeand
user_stats.top_tv_countshape the user statistics, and
cache.user_stats_ttlcaches them. - A link to the played item in Plex from the stream details, which need
Tautulli: aPlexbutton, and the embed title links to the same place. It
opens Plex Web, or the Plex app on a phone. Configure it under
stream_details.links.plex(enabled,base_url,emoji);base_url
accepts any Plex Web, for servers that are not reached through plex.tv. - Privacy options, all defaulting to
falseso every view stays open as
before:stream_details.restrict_to_authorized,
user_stats.restrict_to_authorizedandglobal_stats.restrict_to_authorized
limit those views toDISCORD_AUTHORIZED_USERS, and
stream_details.show_ip_for_authorized_usersshows client IPs to them only. - Configurable bot presence:
presence.enabled,activity_type
(custom,watching,listening),statusandoffline_status(online,
idle,dnd) andauth_failed_text, all defaulting to the 1.x behaviour.
presence.librariestakes library names,"all", and:episodesfor the
episode count. displayoptions:thousands_separatorandepisode_label, replacing
the hard-coded English "Episodes"./mappingcommands to manage the username-to-display-name mapping from
Discord, with an autocomplete that only authorized users get. A flat 1.x
user_mapping.jsonis converted to the platform-keyed format./reload_configto apply config changes without restarting the bot,
including a switch ofmedia_server.type. If the new platform fails to load,
the previous one is restored.!syncto republish the slash commands by hand:guild,copy,
global,clearandclearglobal. A bare!syncmeansguild.- Server uptime that survives a bot restart, kept in
data/runtime_state.json. It is refreshed once a minute while the server is
reachable, so a crash ordocker killkeeps it too, and it starts over when
the bot was away longer thanserver.offline_threshold. - A clean shutdown on
SIGTERM. The bot runs as PID 1 and 1.x installed no
handler, sodocker stopwaited out its grace period and killed it. - SABnzbd: a status icon per download (paused, checking, propagating,
fetching, downloading, can be switched off withshow_status_icons), disk
space in the unit that fits instead of fixed GB and TB,
sabnzbd.show_when_empty, and a choice of which disk SABnzbd reports
(diskspace_free_key,diskspace_total_key). - A test suite and CI: lint, tests and a Docker build on every push and
pull request tomainanddev,ruffas the single lint and format tool,
Dependabot forpipandgithub-actions, and releases cut by release-please.
Every published image is started first and checked for its user and file
permissions. - A
docker-compose.ymlin the repository, andCONTRIBUTING.md,
SECURITY.md, this changelog, issue templates and a pull request template.
Changed
- Renamed from PlexWatch to MediaWatch across code, docs, image path and
repository URL. - The Docker image is based on
python:3.12-sliminstead of
python:3-alpine. - Blocking
plexapicalls run off the event loop, so a slow Plex server no
longer stalls the bot. - Runtime dependencies are constrained to compatible ranges, so a rebuild
cannot pull in a new major version...
v1.1.3 - Enhanced Music Support
What's Changed
- Improved display formatting for music tracks in the PlexWatch dashboard
- Proper artist and track title detection
- Fixed incorrect episode display for music
- Added 🎵 emoji for music entries
- Show bit depth, sample rate, and bitrate for audio
- Cleaner, more informative UI for Plexamp sessions
by @jakedev796 in #10
New Contributors
- @jakedev796 made their first contribution in #10
v1.1.2 - SABnzbd URL Fix
What's Changed
- Fixed SABnzbd API connection issue caused by duplicate slashes in the URL (e.g.,
http://[host]:8082//api) by implementingurllib.parse.urljoinfor robust URL construction. Resolves #9 reported by @CaptSonic.
v1.1.0 - Now with Docker Support
What's Changed
- Added Docker support with
Dockerfile,entrypoint.sh, and updated core files by @ShrinkWrapper in #1 - Updated README with optional SABnzbd and Uptime Kuma configuration by @nichtlegacy
New Contributors
- @ShrinkWrapper made their first contribution in #1