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:

  1. Go to the Discord Developer Portal
  2. Click "New Application" and give it a name (e.g., "Jellyfin Monitor")
  3. Navigate to the "Bot" tab and click "Add Bot"
  4. Under "Privileged Gateway Intents", enable "MESSAGE CONTENT INTENT" (critical for command detection)
  5. Copy your bot token (keep this private, like a password)
  6. 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

  1. In the Discord Developer Portal, go to the "OAuth2" > "URL Generator" tab
  2. Select the scopes: bot and applications.commands
  3. Select the minimum permissions: Send Messages, Read Message History, Mention Everyone
  4. Copy the generated URL and open it in your browser
  5. Select your Discord server and click "Authorize"

Step 3: Set Up Your Python Environment

  1. Create a directory for your project:
mkdir jellyfin-monitor
cd jellyfin-monitor
  1. Create a virtual environment to manage dependencies:
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
  1. 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:

  1. Create a systemd service file (Linux):
sudo nano /etc/systemd/system/jellyfin-monitor.service
  1. 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
  1. Enable and start the service:
sudo systemctl enable jellyfin-monitor.service
sudo systemctl start jellyfin-monitor.service
  1. 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.

Read more