Initial Push

This commit is contained in:
2025-09-05 11:08:00 -05:00
parent a36453f2a9
commit 00a9b09fc6
4 changed files with 486 additions and 0 deletions

19
.env Normal file
View File

@@ -0,0 +1,19 @@
# Discord
DISCORD_TOKEN=your_discord_bot_token
PREFIX=!
GUILD_ID=123456789012345678
ADMIN_ROLE_IDS=111111111111111111,222222222222222222
REQUIRED_ROLE_IDS=333333333333333333,444444444444444444
# Jellyfin
JELLYFIN_URL=http://127.0.0.1:8096
JELLYFIN_API_KEY=your_jellyfin_api_key
# MySQL
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=password
DB_NAME=jellyfin_bot
# Logs
SYNC_LOG_CHANNEL_ID=555555555555555555

View File

@@ -1,2 +1,53 @@
# Jellyfin-Discord # Jellyfin-Discord
Allow the creation and management of Jellyfin users via Discord Allow the creation and management of Jellyfin users via Discord
Join my [Discord](https://discord.com/invite/zJMUNCPtPy) for help, and keeping an eye out for updates!
This is a very simple and lightweight Jellyfin Discord bot for managing users. It allows for creation of accounts, password recovery, account deletion, ect.
Fill out values in the .env and you're good to go!
# Features
- Automatic Account Cleanup
- Creating Accounts
- Recovering Passwords
- Searching accounts by Discord User, or Jellyfin User
- Run Library Scanning
- Manual Account Linking (For previously made Jellyfin accounts)
- Change bot prefix live
# Command Overview
**Pinging the bot will show you the necessary commands to create your account.**
**PLEASE NOTE BEFORE USING. THIS BOT IS MEANT TO USE REQUIRED ROLES IN ORDER TO WHITELIST USERS FOR JELLYFIN. TAKING A USERS ROLE AWAY WILL DELETE THEIR JELLYFIN ACCOUNT WHEN THE BOT RUNS ITS CLEANUP (24 Hour Schedule or Admin Forced)**
![image](https://cdn.pengucc.com/images/projects/jellyfin-bot/ping.png)
**There are protections in place to stop users from creating an account where people can see. If a user sends the account creation or reset in a guild, the bot will delete it.**
![image](https://cdn.pengucc.com/images/projects/jellyfin-bot/account-deny.png)
**If a user already has a linked Jellyfin account, the bot will not allow them to create another account.**
![image](https://cdn.pengucc.com/images/projects/jellyfin-bot/account-limit.png)
**In order to create an account, you must have the required roles specified in the .env**
![image](https://cdn.pengucc.com/images/projects/jellyfin-bot/role-required.png)
***User Commands***
- !createaccount <username> <password> - Create your Jellyfin account
- !recoveraccount <username> <newpassword> - Reset your password
- !deleteaccount <username> - Delete your Jellyfin account
***Admin Commands***
- !syncaccounts - Remove Jellyfin accounts from users without roles
- !searchaccount <jellyfin_username> - Find linked Discord user
- !searchdiscord @user - Find linked Jellyfin account
- !scanlibraries - Scan all Jellyfin libraries
- !assignaccount <jellyfin_username> @user - Manually link accounts
***Admin Bot Commands***
- !setprefix - Change the bots command prefix

412
app.py Normal file
View File

@@ -0,0 +1,412 @@
import discord
from discord.ext import commands, tasks
import requests
import mysql.connector
import asyncio
import os
from dotenv import load_dotenv
# =====================
# ENV + VALIDATION
# =====================
load_dotenv()
def get_env_var(key: str, cast=str, required=True):
value = os.getenv(key)
if required and (value is None or value.strip() == ""):
raise ValueError(f"❌ Missing required environment variable: {key}")
try:
return cast(value) if value is not None else None
except Exception:
raise ValueError(f"❌ Invalid value for {key}, expected {cast.__name__}")
TOKEN = get_env_var("DISCORD_TOKEN")
PREFIX = os.getenv("PREFIX", "!") # Default to "!" if not set
GUILD_ID = get_env_var("GUILD_ID", int)
REQUIRED_ROLE_IDS = [int(x) for x in get_env_var("REQUIRED_ROLE_IDS").split(",")]
ADMIN_ROLE_IDS = [int(x) for x in get_env_var("ADMIN_ROLE_IDS").split(",")]
SYNC_LOG_CHANNEL_ID = get_env_var("SYNC_LOG_CHANNEL_ID", int)
JELLYFIN_URL = get_env_var("JELLYFIN_URL")
JELLYFIN_API_KEY = get_env_var("JELLYFIN_API_KEY")
DB_HOST = get_env_var("DB_HOST")
DB_USER = get_env_var("DB_USER")
DB_PASSWORD = get_env_var("DB_PASSWORD")
DB_NAME = get_env_var("DB_NAME")
# =====================
# DISCORD SETUP
# =====================
intents = discord.Intents.all()
intents.members = True
intents.message_content = True
bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None)
# =====================
# DATABASE SETUP
# =====================
def init_db():
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD
)
cur = conn.cursor()
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{DB_NAME}`")
conn.commit()
cur.close()
conn.close()
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS accounts (
discord_id BIGINT PRIMARY KEY,
jellyfin_username VARCHAR(255) NOT NULL
)
""")
conn.commit()
cur.close()
conn.close()
def add_account(discord_id, jellyfin_username):
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor()
cur.execute("REPLACE INTO accounts (discord_id, jellyfin_username) VALUES (%s, %s)",
(discord_id, jellyfin_username))
conn.commit()
cur.close()
conn.close()
def get_accounts():
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor()
cur.execute("SELECT discord_id, jellyfin_username FROM accounts")
rows = cur.fetchall()
cur.close()
conn.close()
return rows
def get_account_by_jellyfin(username):
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor()
cur.execute("SELECT discord_id FROM accounts WHERE jellyfin_username=%s", (username,))
row = cur.fetchone()
cur.close()
conn.close()
return row
def get_account_by_discord(discord_id):
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor()
cur.execute("SELECT jellyfin_username FROM accounts WHERE discord_id=%s", (discord_id,))
row = cur.fetchone()
cur.close()
conn.close()
return row
def delete_account(discord_id):
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor()
cur.execute("DELETE FROM accounts WHERE discord_id=%s", (discord_id,))
conn.commit()
cur.close()
conn.close()
# =====================
# JELLYFIN HELPERS
# =====================
def create_jellyfin_user(username, password):
headers = {"X-Emby-Token": JELLYFIN_API_KEY}
data = {"Name": username, "Password": password}
r = requests.post(f"{JELLYFIN_URL}/Users/New", json=data, headers=headers)
return r.status_code == 200
def get_jellyfin_user(username):
headers = {"X-Emby-Token": JELLYFIN_API_KEY}
r = requests.get(f"{JELLYFIN_URL}/Users", headers=headers)
if r.status_code == 200:
for u in r.json():
if u["Name"].lower() == username.lower():
return u["Id"]
return None
def delete_jellyfin_user(username):
headers = {"X-Emby-Token": JELLYFIN_API_KEY}
user_id = get_jellyfin_user(username)
if user_id:
r = requests.delete(f"{JELLYFIN_URL}/Users/{user_id}", headers=headers)
return r.status_code in (200, 204)
return True
def reset_jellyfin_password(username: str, new_password: str) -> bool:
user_id = get_jellyfin_user(username)
if not user_id:
return False
headers = {"X-Emby-Token": JELLYFIN_API_KEY}
data = {"Password": new_password}
response = requests.post(f"{JELLYFIN_URL}/Users/{user_id}/Password", headers=headers, json=data)
return response.status_code in (200, 204)
# =====================
# DISCORD HELPERS
# =====================
def has_required_role(member):
return any(role.id in REQUIRED_ROLE_IDS for role in member.roles)
def has_admin_role(member):
return any(role.id in ADMIN_ROLE_IDS for role in member.roles)
# =====================
# EVENTS
# =====================
@bot.event
async def on_message(message):
if message.author == bot.user:
return
if bot.user in message.mentions:
instructions = (
f"👋 Hi {message.author.mention}!\n\n"
"To create a Jellyfin account, please DM me the following command:\n"
f"`{PREFIX}createaccount <username> <password>`\n\n"
"To reset your password, DM me:\n"
f"`{PREFIX}recoveraccount <username> <newpassword>`\n\n"
f"Make sure you have the required server role(s) to create an account."
)
await message.channel.send(instructions)
await bot.process_commands(message)
# =====================
# COMMANDS
# =====================
@bot.command()
async def createaccount(ctx, username: str, password: str):
if not isinstance(ctx.channel, discord.DMChannel):
await ctx.message.delete()
await ctx.send(f"{ctx.author.mention} Please DM me to create your Jellyfin account.")
return
guild = bot.get_guild(GUILD_ID)
member = guild.get_member(ctx.author.id)
if not member or not has_required_role(member):
await ctx.send("❌ You dont have the required role to create an account.")
return
if get_account_by_discord(ctx.author.id):
await ctx.send("❌ You already have a Jellyfin account.")
return
if create_jellyfin_user(username, password):
add_account(ctx.author.id, username)
await ctx.send(f"✅ Account created! You can log in at {JELLYFIN_URL}")
else:
await ctx.send("❌ Failed to create account. Username may already exist.")
@bot.command()
async def recoveraccount(ctx, new_password: str):
"""DM-only: reset your Jellyfin password"""
# Ensure it's a DM
if not isinstance(ctx.channel, discord.DMChannel):
await ctx.message.delete()
await ctx.send(f"{ctx.author.mention} Please DM me to reset your password.")
return
# Fetch the Jellyfin account linked to this Discord user
acc = get_account_by_discord(ctx.author.id)
if not acc:
await ctx.send("❌ You do not have a linked Jellyfin account.")
return
username = acc[0] # the Jellyfin username
# Reset the password
if reset_jellyfin_password(username, new_password):
await ctx.send(
f"✅ Your Jellyfin password for **{username}** has been reset!\n"
f"🌐 Login here: {JELLYFIN_URL}"
)
else:
await ctx.send(f"❌ Failed to reset password for **{username}**. Please contact an admin.")
@bot.command()
async def deleteaccount(ctx, username: str):
if not isinstance(ctx.channel, discord.DMChannel):
await ctx.message.delete()
await ctx.send(f"{ctx.author.mention} Please DM me to delete your Jellyfin account.")
return
acc = get_account_by_discord(ctx.author.id)
if not acc or acc[0].lower() != username.lower():
await ctx.send("❌ That Jellyfin account is not linked to your Discord user.")
return
if delete_jellyfin_user(username):
delete_account(ctx.author.id)
await ctx.send("✅ Account deleted.")
else:
await ctx.send("❌ Failed to delete account.")
@bot.command()
async def syncaccounts(ctx):
guild = bot.get_guild(GUILD_ID)
removed = []
for discord_id, jf_username in get_accounts():
m = guild.get_member(discord_id)
if m is None or not has_required_role(m):
if delete_jellyfin_user(jf_username):
delete_account(discord_id)
removed.append(jf_username)
log_channel = bot.get_channel(SYNC_LOG_CHANNEL_ID)
if removed and log_channel:
await log_channel.send(f"🧹 Removed {len(removed)} Jellyfin accounts: {', '.join(removed)}")
await ctx.send("✅ Sync complete.")
@bot.command()
async def searchaccount(ctx, username: str):
member = ctx.guild.get_member(ctx.author.id)
if not has_admin_role(member):
await ctx.send("❌ You dont have permission to use this command.")
return
result = get_account_by_jellyfin(username)
if result:
discord_id = result[0]
user = await bot.fetch_user(discord_id)
await ctx.send(f"🔍 Jellyfin account **{username}** is linked to Discord user {user.mention}.")
else:
await ctx.send("❌ No linked Discord user found for that Jellyfin account.")
@bot.command()
async def searchdiscord(ctx, user: discord.User):
member = ctx.guild.get_member(ctx.author.id)
if not has_admin_role(member):
await ctx.send("❌ You dont have permission to use this command.")
return
result = get_account_by_discord(user.id)
if result:
await ctx.send(f"🔍 Discord user {user.mention} is linked to Jellyfin account **{result[0]}**.")
else:
await ctx.send("❌ That Discord user does not have a linked Jellyfin account.")
@bot.command()
async def scanlibraries(ctx):
member = ctx.guild.get_member(ctx.author.id)
if not has_admin_role(member):
await ctx.send("❌ You dont have permission to use this command.")
return
headers = {"X-Emby-Token": JELLYFIN_API_KEY}
response = requests.post(f"{JELLYFIN_URL}/Library/Refresh", headers=headers)
if response.status_code in (200, 204):
await ctx.send("✅ All Jellyfin libraries are being scanned.")
else:
await ctx.send(f"❌ Failed to start library scan. Status code: {response.status_code}")
@bot.command()
async def assignaccount(ctx, jellyfin_username: str, user: discord.User):
member = ctx.guild.get_member(ctx.author.id)
if not has_admin_role(member):
await ctx.send("❌ You dont have permission to use this command.")
return
add_account(user.id, jellyfin_username)
await ctx.send(f"✅ Linked Jellyfin account **{jellyfin_username}** to {user.mention}.")
@bot.command()
async def setprefix(ctx, new_prefix: str):
"""Admin-only: change the bot command prefix"""
member = ctx.guild.get_member(ctx.author.id)
if not has_admin_role(member):
await ctx.send("❌ You dont have permission to use this command.")
return
global PREFIX
PREFIX = new_prefix
bot.command_prefix = PREFIX # Update bot prefix dynamically
# Update .env file
env_file = ".env"
lines = []
with open(env_file, "r") as f:
for line in f:
if line.startswith("PREFIX="):
lines.append(f"PREFIX={PREFIX}\n")
else:
lines.append(line)
with open(env_file, "w") as f:
f.writelines(lines)
await ctx.send(f"✅ Command prefix has been updated to `{PREFIX}`")
@bot.command(name="help")
async def help_command(ctx):
member = ctx.guild.get_member(ctx.author.id)
is_admin = has_admin_role(member)
embed = discord.Embed(
title="📖 Jellyfin Bot Help",
description="Here are the available commands:",
color=discord.Color.blue()
)
embed.add_field(name="User Commands", value=(
f"`{PREFIX}createaccount <username> <password>` - Create your Jellyfin account\n"
f"`{PREFIX}recoveraccount <newpassword>` - Reset your password\n"
f"`{PREFIX}deleteaccount <username>` - Delete your Jellyfin account\n"
), inline=False)
if is_admin:
embed.add_field(name="Admin Commands", value=(
f"`{PREFIX}syncaccounts` - Remove Jellyfin accounts from users without roles\n"
f"`{PREFIX}searchaccount <jellyfin_username>` - Find linked Discord user\n"
f"`{PREFIX}searchdiscord @user` - Find linked Jellyfin account\n"
f"`{PREFIX}scanlibraries` - Scan all Jellyfin libraries\n"
f"`{PREFIX}assignaccount <jellyfin_username> @user` - Manually link accounts\n"
), inline=False)
embed.add_field(name="Admin Bot Commands", value=(
f"`{PREFIX}setprefix` - Change the bots command prefix\n"
), inline=False)
await ctx.send(embed=embed)
# =====================
# TASKS
# =====================
@tasks.loop(hours=24)
async def daily_check():
guild = bot.get_guild(GUILD_ID)
removed = []
for discord_id, jf_username in get_accounts():
m = guild.get_member(discord_id)
if m is None or not has_required_role(m):
if delete_jellyfin_user(jf_username):
delete_account(discord_id)
removed.append(jf_username)
if removed:
print(f"Daily cleanup: removed {len(removed)} accounts: {removed}")
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
init_db()
daily_check.start()
await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=f"{PREFIX}help"))
bot.run(TOKEN)

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
discord.py==2.3.2
requests==2.32.3
mysql-connector-python==9.0.0
python-dotenv==1.0.1