Building a Discord Bot to Monitor Your Jellyfin Media Server
Jellyfin is a popular self-hosted media server solution, but without proper monitoring, you might not know when it goes down until you try to watch something. This guide will walk you through creating a Discord bot that monitors your Jellyfin server and sends notifications when issues are detected.
What You'll Build
A Discord bot that:
- Checks your Jellyfin server's status regularly
- Sends notifications when your server goes down or recovers
- Provides a daily status update (configurable time)
- Responds to commands for checking status and uptime
- Notifies the server owner on status changes
- Displays detailed status information with visual indicators
Prerequisites
- A server/computer to run the Discord bot (can be the same as your Jellyfin server)
- Python 3.8+ installed
- A Discord account with permissions to create applications
- Basic knowledge of command line operations
- SSH access to your Jellyfin server (if monitoring remotely)
Step 1: Set Up a Discord Bot
First, create a Discord bot application:
- Go to the Discord Developer Portal
- Click "New Application" and give it a name (e.g., "Jellyfin Monitor")
- Navigate to the "Bot" tab and click "Add Bot"
- Under "Privileged Gateway Intents", enable "MESSAGE CONTENT INTENT" (critical for command detection)
- Copy your bot token (keep this private, like a password)
- Under "Bot Permissions", ensure the bot has at minimum:
- Send Messages
- Read Message History
- Mention Everyone (for tagging server owner)
Step 2: Invite the Bot to Your Server
- In the Discord Developer Portal, go to the "OAuth2" > "URL Generator" tab
- Select the scopes:
botandapplications.commands - Select the minimum permissions: Send Messages, Read Message History, Mention Everyone
- Copy the generated URL and open it in your browser
- Select your Discord server and click "Authorize"
Step 3: Set Up Your Python Environment
- Create a directory for your project:
mkdir jellyfin-monitor
cd jellyfin-monitor
- Create a virtual environment to manage dependencies:
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
- Install the required packages:
pip install discord.py requests apscheduler ping3
Step 4: Create the Bot Script
Create a file named jellyfin_monitor.py and paste the following code:
#!/usr/bin/env python3
import os
import time
import logging
import requests
import subprocess
import socket
import ping3
from datetime import datetime
import discord
from discord.ext import commands
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
import asyncio
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("jellyfin_monitor.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("jellyfin_monitor")
# Configuration variables - update these with your own settings
DISCORD_TOKEN = "YOUR_DISCORD_BOT_TOKEN" # Replace with your token
CHANNEL_ID = 123456789012345678 # Replace with your Discord channel ID
JELLYFIN_URL = "http://your-jellyfin-server:8096" # Update with Jellyfin URL
DOCKER_CONTAINER_NAME = "jellyfin" # Name of Docker container (if using Docker)
CHECK_INTERVAL_MINUTES = 60 # Check every hour
# Server Configuration
SERVER_IP = "your-server-ip" # IP address of server hosting Jellyfin
SSH_PORT = 22 # SSH port
SSH_USER = "your-ssh-username"
SSH_IDENTITY_FILE = "/path/to/your/ssh/key" # Path to SSH key
SSH_TIMEOUT = 5 # Timeout in seconds for SSH connection attempts
# Global state tracker
last_known_state = {
"status": "unknown", # 'up', 'down', 'restarting', 'server_down', 'docker_down'
"last_notification_time": None,
"last_daily_up_notification": None,
}
# Initialize Discord bot with proper intents
intents = discord.Intents.default()
intents.message_content = True # Enable message content intent
bot = commands.Bot(command_prefix='!', intents=intents)
async def send_discord_message(message):
"""Send a message to the configured Discord channel."""
channel = bot.get_channel(CHANNEL_ID)
if channel:
await channel.send(message)
logger.info(f"Message sent to Discord: {message}")
else:
logger.error(f"Failed to find Discord channel with ID: {CHANNEL_ID}")
def check_server_reachable():
"""Check if the server is reachable via ping and SSH."""
# First try ping
try:
result = ping3.ping(SERVER_IP, timeout=2)
if result is None or result is False:
logger.warning(f"Server at {SERVER_IP} not responding to ping")
return False
except Exception as e:
logger.warning(f"Error pinging server: {e}")
# Continue to SSH check even if ping fails (ping might be disabled)
# Then try SSH connection
try:
cmd = f"ssh -i {SSH_IDENTITY_FILE} -p {SSH_PORT} -o ConnectTimeout={SSH_TIMEOUT} -o BatchMode=yes -o StrictHostKeyChecking=no {SSH_USER}@{SERVER_IP} 'echo Connection test'"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=SSH_TIMEOUT+2)
if result.returncode == 0:
return True
else:
logger.warning(f"SSH to server failed: {result.stderr}")
return False
except subprocess.TimeoutExpired:
logger.warning("SSH connection to server timed out")
return False
except Exception as e:
logger.error(f"Error checking server SSH: {e}")
return False
def check_docker_running():
"""Check if Docker service is running on the server."""
try:
cmd = f"ssh -i {SSH_IDENTITY_FILE} -p {SSH_PORT} -o ConnectTimeout={SSH_TIMEOUT} {SSH_USER}@{SERVER_IP} 'systemctl is-active docker'"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=SSH_TIMEOUT+2)
return result.returncode == 0 and result.stdout.strip() == "active"
except Exception as e:
logger.error(f"Error checking Docker service: {e}")
return False
def check_docker_status():
"""Check if the Jellyfin Docker container is running, restarting, or stopped."""
try:
# Execute docker command via SSH
cmd = f"ssh -i {SSH_IDENTITY_FILE} -p {SSH_PORT} {SSH_USER}@{SERVER_IP} 'docker inspect --format={{{{.State.Status}}}} {DOCKER_CONTAINER_NAME}'"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
logger.error(f"Docker command failed: {result.stderr}")
return "unknown"
status = result.stdout.strip()
if status == "restarting":
return "restarting"
elif status == "running":
return "running"
else:
return "stopped"
except Exception as e:
logger.error(f"Error checking Docker status: {e}")
return "unknown"
def get_container_uptime():
"""Get the uptime of the Jellyfin Docker container."""
try:
# Get the container start time
cmd = f"ssh -i {SSH_IDENTITY_FILE} -p {SSH_PORT} {SSH_USER}@{SERVER_IP} 'docker inspect --format={{{{.State.StartedAt}}}} {DOCKER_CONTAINER_NAME}'"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
logger.error(f"Docker command failed: {result.stderr}")
return None
started_at = result.stdout.strip()
# Parse the timestamp (format: 2023-05-17T12:34:56.789012345Z)
if started_at:
try:
# Remove the nanoseconds and Z
started_at = started_at.split('.')[0]
# Parse the timestamp
started_time = datetime.strptime(started_at, "%Y-%m-%dT%H:%M:%S")
# Calculate uptime
uptime = datetime.now() - started_time
return uptime
except Exception as e:
logger.error(f"Error parsing container start time: {e}")
return None
return None
except Exception as e:
logger.error(f"Error getting container uptime: {e}")
return None
def check_jellyfin_health():
"""Check if Jellyfin is responding to HTTP requests."""
try:
response = requests.get(f"{JELLYFIN_URL}/health", timeout=10)
if response.status_code == 200:
return True
return False
except requests.RequestException:
return False
async def check_jellyfin_status():
"""
Check the status of Jellyfin server and send notifications to Discord
based on state changes or scheduled updates.
"""
global last_known_state
current_time = datetime.now()
# Check if server is reachable
if not check_server_reachable():
current_status = "server_down"
logger.warning("Server is unreachable")
# Then check if Docker service is running
elif not check_docker_running():
current_status = "docker_down"
logger.warning("Docker service is not running")
else:
# Check the Docker container status
docker_status = check_docker_status()
# Determine the actual status of Jellyfin
if docker_status == "restarting":
current_status = "restarting"
elif docker_status == "running":
# Container is running, but let's check if Jellyfin is actually responding
if check_jellyfin_health():
current_status = "up"
else:
# Container is running but Jellyfin is not responding
current_status = "down"
else:
current_status = "down"
logger.info(f"Current Jellyfin status: {current_status}")
# Handle notifications based on status changes
if current_status != last_known_state["status"]:
if current_status == "server_down":
await send_discord_message("🔴 **Critical Alert**: The server hosting Jellyfin is DOWN or unreachable!")
elif current_status == "docker_down":
await send_discord_message("🟠 **Alert**: Docker service on the Jellyfin server is not running!")
elif current_status == "down" and last_known_state["status"] != "down":
# Jellyfin just went down
await send_discord_message("⚠️ **Alert**: Jellyfin server is DOWN! The service is not responding.")
elif current_status == "restarting" and last_known_state["status"] != "restarting":
# Jellyfin is restarting
await send_discord_message("🔄 **Info**: Jellyfin server is currently RESTARTING.")
elif current_status == "up" and (last_known_state["status"] in ["down", "server_down", "docker_down", "restarting", "unknown"]):
# Jellyfin just came back up after being down
downtime = "unknown"
if last_known_state["last_notification_time"]:
downtime_seconds = (current_time - last_known_state["last_notification_time"]).total_seconds()
hours, remainder = divmod(downtime_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
downtime = f"{int(hours)}h {int(minutes)}m {int(seconds)}s"
previous_state = last_known_state["status"]
await send_discord_message(f"✅ **Good News**: Jellyfin server is back UP! (Previous state: {previous_state}, Downtime: {downtime})")
# Notify server owner for non-UP statuses
if current_status != "up":
await notify_server_owner(current_status)
# Update notification time
last_known_state["last_notification_time"] = current_time
# Update the last known state
last_known_state["status"] = current_status
async def notify_server_owner(status):
"""Notify the server owner when Jellyfin has a non-UP status."""
channel = bot.get_channel(CHANNEL_ID)
if not channel:
logger.error(f"Failed to find Discord channel with ID: {CHANNEL_ID}")
return
guild = channel.guild
owner = guild.owner
if not owner:
logger.error("Could not determine server owner")
return
status_messages = {
"down": "⚠️ Your Jellyfin server is DOWN! The service is not responding.",
"restarting": "🔄 Your Jellyfin server is currently RESTARTING.",
"server_down": "🔴 CRITICAL: The server hosting Jellyfin is DOWN or unreachable!",
"docker_down": "🟠 Your Docker service on the Jellyfin server is not running!",
"unknown": "❓ Your Jellyfin server status is UNKNOWN. Something may be wrong."
}
message = status_messages.get(status, f"⚠️ Your Jellyfin server has an issue: {status.upper()}")
try:
# Mention the owner in the channel
await channel.send(f"{owner.mention} {message}")
logger.info(f"Notified server owner {owner.name} about status: {status}")
except discord.Forbidden:
logger.error("Bot doesn't have permission to mention the server owner")
except Exception as e:
logger.error(f"Error notifying server owner: {e}")
async def daily_up_notification():
"""Send a daily notification if Jellyfin is up."""
if last_known_state["status"] == "up":
await send_discord_message("📊 **Daily Status**: Jellyfin server is running normally.")
last_known_state["last_daily_up_notification"] = datetime.now()
logger.info("Sent daily UP notification")
else:
logger.info(f"Skipped daily UP notification as server is not up (current status: {last_known_state['status']})")
@bot.event
async def on_ready():
"""Run when the bot is ready and connected to Discord."""
logger.info(f"Logged in as {bot.user.name} ({bot.user.id})")
# Set up the scheduler
scheduler = AsyncIOScheduler()
# Schedule hourly checks
scheduler.add_job(
check_jellyfin_status,
'interval',
minutes=CHECK_INTERVAL_MINUTES,
id='jellyfin_hourly_check'
)
# Schedule daily 8 AM status update
scheduler.add_job(
daily_up_notification,
CronTrigger(hour=8, minute=0),
id='jellyfin_daily_status'
)
# Perform an initial check on startup
await check_jellyfin_status()
# Start the scheduler
scheduler.start()
logger.info("Scheduled jobs have been started")
@bot.command(name="status")
async def status_command(ctx):
"""Manually check and report Jellyfin status."""
logger.info(f"Status command received from {ctx.author}")
await ctx.send("Checking Jellyfin status...")
await check_jellyfin_status()
status_emojis = {
"up": "✅",
"down": "⚠️",
"restarting": "🔄",
"server_down": "🔴",
"docker_down": "🟠",
"unknown": "❓"
}
emoji = status_emojis.get(last_known_state["status"], "❓")
status_message = last_known_state["status"].upper().replace("_", " ")
await ctx.send(f"{emoji} Jellyfin is currently **{status_message}**")
@bot.command(name="jellyfin_help")
async def jellyfin_help_command(ctx):
"""Show available commands and bot information."""
logger.info(f"Help command received from {ctx.author}")
help_text = """
**Jellyfin Monitor Bot Commands**
`!status` - Check the current status of the Jellyfin server
`!uptime` - Show how long the Jellyfin server has been running
`!jellyfin_help` - Show this help message
**Automatic Monitoring Features**
• Hourly status checks
• Status change notifications
• Daily status report at 8:00 AM
• Detailed status indicators:
✅ UP - Everything is working properly
⚠️ DOWN - Jellyfin is not responding
🔄 RESTARTING - Jellyfin container is restarting
🟠 DOCKER DOWN - Docker service is not running
🔴 SERVER DOWN - The entire server is unreachable
"""
await ctx.send(help_text)
@bot.command(name="uptime")
async def uptime_command(ctx):
"""Show how long the Jellyfin server has been running."""
logger.info(f"Uptime command received from {ctx.author}")
# Check current status first
if last_known_state["status"] != "up":
status_message = last_known_state["status"].upper().replace("_", " ")
await ctx.send(f"⚠️ Jellyfin is currently **{status_message}**, so uptime information is not available.")
return
await ctx.send("Checking Jellyfin uptime...")
# Get container uptime
uptime = get_container_uptime()
if uptime:
# Format uptime nicely
days = uptime.days
hours, remainder = divmod(uptime.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
if days > 0:
uptime_str = f"{days} days, {hours} hours, {minutes} minutes"
elif hours > 0:
uptime_str = f"{hours} hours, {minutes} minutes"
else:
uptime_str = f"{minutes} minutes, {seconds} seconds"
await ctx.send(f"⏱️ Jellyfin has been running for **{uptime_str}**")
else:
await ctx.send("❌ Unable to retrieve uptime information. The server might be running, but uptime data couldn't be accessed.")
# Run the bot
if __name__ == "__main__":
try:
bot.run(DISCORD_TOKEN)
except Exception as e:
logger.critical(f"Bot crashed: {e}")
Step 5: Configure the Script
Edit the script to update the configuration variables with your own values:
# Configuration variables
DISCORD_TOKEN = "your-discord-bot-token" # From Discord Developer Portal
CHANNEL_ID = 123456789012345678 # Your Discord channel ID (numeric)
JELLYFIN_URL = "http://your-jellyfin-server:8096" # Your Jellyfin URL
DOCKER_CONTAINER_NAME = "jellyfin" # Only if using Docker
# Server access details
SERVER_IP = "192.168.1.100" # IP of your Jellyfin server
SSH_PORT = 22 # SSH port
SSH_USER = "your-username" # SSH username
SSH_IDENTITY_FILE = "/path/to/your/ssh/key" # Path to SSH key
Step 6: Test Run the Bot
Before setting up the bot to run continuously, test it to make sure everything works:
# Make sure your virtual environment is activated
source venv/bin/activate # On Windows: venv\Scripts\activate
# Run the script
python jellyfin_monitor.py
The bot should connect to Discord and perform an initial status check of your Jellyfin server.
Step 7: Set Up as a System Service
To run the bot as a background service that starts automatically on system boot:
- Create a systemd service file (Linux):
sudo nano /etc/systemd/system/jellyfin-monitor.service
- Add the following configuration (adjust paths as needed):
[Unit]
Description=Jellyfin Discord Monitor Bot
After=network.target
[Service]
Type=simple
User=your_username # Change to your username
WorkingDirectory=/path/to/jellyfin-monitor
ExecStart=/path/to/jellyfin-monitor/venv/bin/python /path/to/jellyfin-monitor/jellyfin_monitor.py
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
- Enable and start the service:
sudo systemctl enable jellyfin-monitor.service
sudo systemctl start jellyfin-monitor.service
- Check the status to ensure it's running:
sudo systemctl status jellyfin-monitor.service
Using the Bot
The bot provides several commands you can use in your Discord server:
!status- Check the current status of the Jellyfin server!uptime- Show how long the Jellyfin server has been running!jellyfin_help- Display help information
The bot will also automatically:
- Check your Jellyfin server status hourly (configurable)
- Send notifications when the server status changes
- Provide a daily status report at 8:00 AM
- Alert the server owner when there are issues
Troubleshooting
Bot Not Responding to Commands
If the bot doesn't respond to commands, check that:
- The "MESSAGE CONTENT INTENT" is enabled in the Discord Developer Portal
- The bot has permission to send messages in the channel
- The channel ID in the configuration is correct
SSH Connection Issues
If you have SSH connection issues:
- Ensure the SSH key has the correct permissions (chmod 600)
- Verify the server IP and SSH port are correct
- Make sure the SSH user has the necessary permissions on the server
Permission Problems
If the bot is running but can't send messages, check that:
- The bot has "Send Messages" permission in the Discord channel
- For owner notifications, the bot needs "Mention Everyone" permission
Bot Crashes on Startup
If the bot crashes immediately:
- Check the logs with:
journalctl -u jellyfin-monitor.service -n 50 - Verify that all required Python packages are installed
- Ensure your Discord token is correct
Customization
Change Check Frequency
To change how often the bot checks your Jellyfin server, modify the CHECK_INTERVAL_MINUTES variable.
Modify Daily Status Time
To change when the daily status report is sent, modify the CronTrigger parameters:
scheduler.add_job(
daily_up_notification,
CronTrigger(hour=9, minute=30), # Changed to 9:30 AM
id='jellyfin_daily_status'
)
Add Custom Commands
You can extend the bot with additional commands by adding more functions with the @bot.command() decorator.
Conclusion
You now have a Discord bot that monitors your Jellyfin server and keeps you informed about its status. This setup provides peace of mind, ensuring you'll know immediately if your media server experiences any issues.
The bot's multi-layer monitoring approach can detect problems at the server level, Docker service level, or Jellyfin application level, giving you detailed information about any issues that arise.