11 Commits
1.0.2 ... 1.0.4

Author SHA1 Message Date
2624f1ec06 1.0.4 2025-09-06 22:11:42 -05:00
bfb16c503a Multi-Guild Support 2025-09-06 22:04:15 -05:00
9acbbe5644 Bump 1.0.3 2025-09-06 19:50:18 -05:00
2d2e649a5f Update README.md 2025-09-06 19:48:10 -05:00
a6f110c9c8 What is wrong with me. 2025-09-06 19:47:07 -05:00
2621fa45e5 Update Readme 2025-09-06 19:41:58 -05:00
f1acaf529a Update Repo Name 2025-09-06 18:50:24 -05:00
d970f37343 Update app.py 2025-09-06 17:45:45 -05:00
0ef8fe58d2 Update Markdown 2025-09-06 17:28:35 -05:00
24190a3b4a Trial accounts 2025-09-06 17:05:24 -05:00
4de1e6e7cb Fixing more things I forgot
- Fixed: ValueError: too many values to unpack (expected 2)
- Cleanup will now delete Jellyseerr accounts as well
2025-09-06 12:47:11 -05:00
6 changed files with 442 additions and 55 deletions

3
.env
View File

@@ -1,13 +1,14 @@
# Discord # Discord
DISCORD_TOKEN=your_discord_bot_token DISCORD_TOKEN=your_discord_bot_token
PREFIX=! PREFIX=!
GUILD_ID=123456789012345678 GUILD_ID=123456789012345678,123456789012345678
ADMIN_ROLE_IDS=111111111111111111,222222222222222222 ADMIN_ROLE_IDS=111111111111111111,222222222222222222
REQUIRED_ROLE_IDS=333333333333333333,444444444444444444 REQUIRED_ROLE_IDS=333333333333333333,444444444444444444
# Jellyfin # Jellyfin
JELLYFIN_URL=http://127.0.0.1:8096 JELLYFIN_URL=http://127.0.0.1:8096
JELLYFIN_API_KEY=your_jellyfin_api_key JELLYFIN_API_KEY=your_jellyfin_api_key
ENABLE_TRIAL_ACCOUNTS=false
# Jellyseerr # Jellyseerr
JELLYSEERR_ENABLED=false JELLYSEERR_ENABLED=false

27
CHANGELOG.md Normal file
View File

@@ -0,0 +1,27 @@
# 1.0.4
- Multi-Guild support (As long as a user has a required role or admin role in one server, they are able to use the bot and Jellyfin)
- Fixed link command instructions not updating based on Jellyseerr availability
# 1.0.3
- Fixed: `ValueError: too many values to unpack (expected 2)`
- Cleanup will now delete Jellyseerr accounts as well
- Added Trial Jellyfin account support (enable in .env). Will not create a Jellyseerr account, lasts 24 hours, one time use.
# 1.0.2
- Fixed Jellyseerr support breaking linking, unlinking, and deletion
- Added the ability to link Jellyseerr accounts when linking Jellyfin
- Added event logging support for console (Can be toggled in .env or with commands)
- Reformatted the updates message
- Running a command without any values will now show you the proper command usage
# 1.0.1
- Added Jellyseerr Support
# 1.0.0
- Bot can now track update releases
- Restrict prefixes to non-alphanumeric symbols

View File

@@ -1,4 +1,10 @@
# Jellyfin-Discord # Jellycord
![image](https://cdn.pengucc.com/images/projects/jellycord/readme/BannerRound.png)
[![Online Members](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscordapp.com%2Fapi%2Finvites%2FEdPJAhrDq8%3Fwith_counts%3Dtrue&query=approximate_presence_count&style=for-the-badge&logo=discord&logoColor=white&label=ONLINE%20MEMBERS&labelColor=grey&color=239eda)](https://discord.gg/EdPJAhrDq8)
![Latest Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FPenguCCN%2FJellycord%2Fmain%2Fversion.json&query=%24.version&style=for-the-badge&logo=python&logoColor=white&label=Latest%20Version%3A&color=239eda)
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! Join my [Discord](https://discord.com/invite/zJMUNCPtPy) for help, and keeping an eye out for updates!
@@ -7,7 +13,7 @@ This is a very simple and lightweight Jellyfin Discord bot for managing users. I
Fill out values in the .env and you're good to go! Fill out values in the .env and you're good to go!
# Features ## Features
- Automatic Account Cleanup - Automatic Account Cleanup
- Creating Accounts - Creating Accounts
@@ -18,30 +24,31 @@ Fill out values in the .env and you're good to go!
- Change bot prefix live - Change bot prefix live
- Checks for new releases - Checks for new releases
# Command Overview ## Command Overview
**Pinging the bot will show you the necessary commands to create your account.** **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)** **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) ![image](https://cdn.pengucc.com/images/projects/jellycord/readme/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.** **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) ![image](https://cdn.pengucc.com/images/projects/jellycord/readme/account-deny.png)
**If a user already has a linked Jellyfin account, the bot will not allow them to create another account.** **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) ![image](https://cdn.pengucc.com/images/projects/jellycord/readme/account-limit.png)
**In order to create an account, you must have the required roles specified in the .env** **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) ![image](https://cdn.pengucc.com/images/projects/jellycord/readme/role-required.png)
***User Commands*** ***User Commands***
- `!createaccount` <username> <password> - Create your Jellyfin account - `!createaccount` <username> <password> - Create your Jellyfin account
- `!recoveraccount` <username> <newpassword> - Reset your password - `!recoveraccount` <username> <newpassword> - Reset your password
- `!deleteaccount` <username> - Delete your Jellyfin account - `!deleteaccount` <username> - Delete your Jellyfin account
- `!trialaccount` <username> <password> - Create a 24-hour trial Jellyfin account. Only if ENABLE_TRIAL_ACCOUNTS=True
***Admin Commands*** ***Admin Commands***
- `!cleanup` - Remove Jellyfin accounts from users without roles - `!cleanup` - Remove Jellyfin accounts from users without roles

441
app.py
View File

@@ -22,13 +22,14 @@ def get_env_var(key: str, cast=str, required=True):
TOKEN = get_env_var("DISCORD_TOKEN") TOKEN = get_env_var("DISCORD_TOKEN")
PREFIX = os.getenv("PREFIX", "!") # Default to "!" if not set PREFIX = os.getenv("PREFIX", "!") # Default to "!" if not set
GUILD_ID = get_env_var("GUILD_ID", int) GUILD_IDS = [int(x.strip()) for x in get_env_var("GUILD_IDS").split(",")]
REQUIRED_ROLE_IDS = [int(x) for x in get_env_var("REQUIRED_ROLE_IDS").split(",")] 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(",")] 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) SYNC_LOG_CHANNEL_ID = get_env_var("SYNC_LOG_CHANNEL_ID", int)
JELLYFIN_URL = get_env_var("JELLYFIN_URL") JELLYFIN_URL = get_env_var("JELLYFIN_URL")
JELLYFIN_API_KEY = get_env_var("JELLYFIN_API_KEY") JELLYFIN_API_KEY = get_env_var("JELLYFIN_API_KEY")
ENABLE_TRIAL_ACCOUNTS = os.getenv("ENABLE_TRIAL_ACCOUNTS", "False").lower() == "true"
JELLYSEERR_ENABLED = os.getenv("JELLYSEERR_ENABLED", "false").lower() == "true" JELLYSEERR_ENABLED = os.getenv("JELLYSEERR_ENABLED", "false").lower() == "true"
JELLYSEERR_URL = os.getenv("JELLYSEERR_URL", "").rstrip("/") JELLYSEERR_URL = os.getenv("JELLYSEERR_URL", "").rstrip("/")
@@ -39,9 +40,9 @@ DB_USER = get_env_var("DB_USER")
DB_PASSWORD = get_env_var("DB_PASSWORD") DB_PASSWORD = get_env_var("DB_PASSWORD")
DB_NAME = get_env_var("DB_NAME") DB_NAME = get_env_var("DB_NAME")
BOT_VERSION = "1.0.2" BOT_VERSION = "1.0.4"
VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellyfin-Discord/main/version.txt" VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt"
RELEASES_URL = "https://github.com/PenguCCN/Jellyfin-Discord/releases" RELEASES_URL = "https://github.com/PenguCCN/Jellycord/releases"
# ===================== # =====================
# EVENT LOGGING # EVENT LOGGING
@@ -65,25 +66,27 @@ bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None)
# ===================== # =====================
def init_db(): def init_db():
log_event(f"Initiating Database...") log_event(f"Initiating Database...")
# Create database if it doesn't exist conn = mysql.connector.connect(
conn = mysql.connector.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD) host=DB_HOST, user=DB_USER, password=DB_PASSWORD
)
cur = conn.cursor() cur = conn.cursor()
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{DB_NAME}`") cur.execute(f"CREATE DATABASE IF NOT EXISTS `{DB_NAME}`")
conn.commit() conn.commit()
cur.close() cur.close()
conn.close() conn.close()
# Connect to the database conn = mysql.connector.connect(
conn = mysql.connector.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME) host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor() cur = conn.cursor()
# Create accounts table if it doesn't exist # Normal accounts table
cur.execute(""" cur.execute("""
CREATE TABLE IF NOT EXISTS accounts ( CREATE TABLE IF NOT EXISTS accounts (
discord_id BIGINT PRIMARY KEY, discord_id BIGINT PRIMARY KEY,
jellyfin_username VARCHAR(255) NOT NULL, jellyfin_username VARCHAR(255) NOT NULL,
jellyfin_id VARCHAR(255) NOT NULL, jellyfin_id VARCHAR(255) NOT NULL,
jellyseerr_id VARCHAR(255) DEFAULT NULL jellyseerr_id VARCHAR(255)
) )
""") """)
@@ -99,7 +102,18 @@ def init_db():
cur.execute("ALTER TABLE accounts ADD COLUMN jellyseerr_id VARCHAR(255) DEFAULT NULL") cur.execute("ALTER TABLE accounts ADD COLUMN jellyseerr_id VARCHAR(255) DEFAULT NULL")
print("[DB] Added missing column 'jellyseerr_id' to accounts table.") print("[DB] Added missing column 'jellyseerr_id' to accounts table.")
# Create bot_metadata table if it doesn't exist # Trial accounts table (persistent history, one-time only)
cur.execute("""
CREATE TABLE IF NOT EXISTS trial_accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
discord_id BIGINT NOT NULL UNIQUE,
jellyfin_username VARCHAR(255),
jellyfin_id VARCHAR(255),
trial_created_at DATETIME NOT NULL,
expired BOOLEAN DEFAULT 0
)
""")
cur.execute(""" cur.execute("""
CREATE TABLE IF NOT EXISTS bot_metadata ( CREATE TABLE IF NOT EXISTS bot_metadata (
key_name VARCHAR(255) PRIMARY KEY, key_name VARCHAR(255) PRIMARY KEY,
@@ -107,6 +121,14 @@ def init_db():
) )
""") """)
# Cleanup logs table
cur.execute("""
CREATE TABLE IF NOT EXISTS cleanup_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
run_at DATETIME NOT NULL
)
""")
conn.commit() conn.commit()
cur.close() cur.close()
conn.close() conn.close()
@@ -125,6 +147,26 @@ def add_account(discord_id, username, jf_id, js_id=None):
cur.close() cur.close()
conn.close() conn.close()
def init_trial_accounts_table():
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor()
# Persistent trial accounts table
cur.execute("""
CREATE TABLE IF NOT EXISTS trial_accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
discord_id BIGINT NOT NULL UNIQUE,
jellyfin_username VARCHAR(255) NOT NULL,
jellyfin_id VARCHAR(255) NOT NULL,
trial_created_at DATETIME NOT NULL,
expired BOOLEAN DEFAULT 0
)
""")
conn.commit()
cur.close()
conn.close()
def get_accounts(): def get_accounts():
conn = mysql.connector.connect( conn = mysql.connector.connect(
@@ -269,11 +311,30 @@ def delete_jellyseerr_user(js_id: str) -> bool:
# ===================== # =====================
# DISCORD HELPERS # 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): def has_required_role(user: discord.User | discord.Member) -> bool:
return any(role.id in ADMIN_ROLE_IDS for role in member.roles) """Check if the user has any of the required roles across all configured guilds."""
for gid in GUILD_IDS:
guild = bot.get_guild(gid)
if not guild:
continue
member = guild.get_member(user.id)
if member and any(role.id in REQUIRED_ROLE_IDS for role in member.roles):
return True
return False
def has_admin_role(user: discord.User | discord.Member) -> bool:
"""Check if the user has any of the admin roles across all configured guilds."""
for gid in GUILD_IDS:
guild = bot.get_guild(gid)
if not guild:
continue
member = guild.get_member(user.id)
if member and any(role.id in ADMIN_ROLE_IDS for role in member.roles):
return True
return False
# ===================== # =====================
# BOT HELPERS # BOT HELPERS
@@ -302,6 +363,33 @@ def get_metadata(key):
conn.close() conn.close()
return row[0] if row else None return row[0] if row else None
def create_trial_jellyfin_user(username, password):
payload = {
"Name": username,
"Password": password,
"Policy": {
"EnableDownloads": False,
"EnableSyncTranscoding": False,
"EnableRemoteControlOfOtherUsers": False,
"EnableLiveTvAccess": False,
"IsAdministrator": False,
"IsHidden": False,
"IsDisabled": False
}
}
headers = {
"X-Emby-Token": JELLYFIN_API_KEY,
"Content-Type": "application/json"
}
response = requests.post(f"{JELLYFIN_URL}/Users/New", json=payload, headers=headers)
if response.status_code == 200:
return response.json().get("Id")
else:
print(f"[Jellyfin] Trial user creation failed. Status: {response.status_code}, Response: {response.text}")
return None
# ===================== # =====================
# EVENTS # EVENTS
# ===================== # =====================
@@ -349,8 +437,14 @@ async def createaccount(ctx, username: str = None, password: str = None):
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to create your Jellyfin account.") await ctx.send(f"{ctx.author.mention} ❌ Please DM me to create your Jellyfin account.")
return return
guild = bot.get_guild(GUILD_ID) member = None
member = guild.get_member(ctx.author.id) if guild else None for gid in GUILD_IDS:
guild = bot.get_guild(gid)
if guild:
member = guild.get_member(ctx.author.id)
if member and has_required_role(member):
break
if not member or not has_required_role(member): if not member or not has_required_role(member):
await ctx.send(f"{ctx.author.mention}, you dont have the required role.") await ctx.send(f"{ctx.author.mention}, you dont have the required role.")
return return
@@ -381,6 +475,83 @@ async def createaccount(ctx, username: str = None, password: str = None):
else: else:
await ctx.send(f"❌ Failed to create Jellyfin account **{username}**. It may already exist.") await ctx.send(f"❌ Failed to create Jellyfin account **{username}**. It may already exist.")
@bot.command()
async def trialaccount(ctx, username: str = None, password: str = None):
"""Create a 24-hour trial Jellyfin account. DM-only, one-time per user."""
log_event(f"trialaccount invoked by {ctx.author}")
# Ensure trial accounts are enabled
if not ENABLE_TRIAL_ACCOUNTS:
await ctx.send("❌ Trial accounts are currently disabled.")
return
# Ensure it's a DM
if not isinstance(ctx.channel, discord.DMChannel):
try:
await ctx.message.delete()
except discord.Forbidden:
pass
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to create a trial account.")
return
# Ensure required arguments
if username is None or password is None:
await ctx.send(command_usage(f"{PREFIX}trialaccount", ["<username>", "<password>"]))
return
member = None
for gid in GUILD_IDS:
guild = bot.get_guild(gid)
if guild:
member = guild.get_member(ctx.author.id)
if member and has_required_role(member):
break
if not member or not has_required_role(member):
await ctx.send(f"{ctx.author.mention}, you dont have the required role.")
return
# Check if user already has a normal Jellyfin account
if get_account_by_discord(ctx.author.id):
await ctx.send(f"{ctx.author.mention}, you already have a Jellyfin account.")
return
# Check if user already had a trial account (one-time)
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor()
cur.execute("SELECT * FROM trial_accounts WHERE discord_id=%s", (ctx.author.id,))
existing_trial = cur.fetchone()
if existing_trial:
cur.close()
conn.close()
await ctx.send(f"{ctx.author.mention}, you have already used your trial account. You cannot create another.")
return
# Create Jellyfin trial user
if create_jellyfin_user(username, password):
jf_id = get_jellyfin_user(username)
if not jf_id:
await ctx.send(f"❌ Failed to fetch Jellyfin ID for **{username}**. Please contact an admin.")
return
# Store trial account info in separate persistent table
cur.execute("""
INSERT INTO trial_accounts (discord_id, jellyfin_username, jellyfin_id, trial_created_at, expired)
VALUES (%s, %s, %s, NOW(), 0)
""", (ctx.author.id, username, jf_id))
conn.commit()
cur.close()
conn.close()
await ctx.send(f"✅ Trial Jellyfin account **{username}** created! It will expire in 24 hours.\n🌐 Login here: {JELLYFIN_URL}")
log_event(f"Trial account created for {ctx.author} ({username})")
else:
cur.close()
conn.close()
await ctx.send(f"❌ Failed to create trial account **{username}**. It may already exist.")
@bot.command() @bot.command()
async def recoveraccount(ctx, new_password: str = None): async def recoveraccount(ctx, new_password: str = None):
@@ -443,13 +614,30 @@ async def deleteaccount(ctx, username: str = None):
@bot.command() @bot.command()
async def cleanup(ctx): async def cleanup(ctx):
log_event(f"cleanup invoked by {ctx.author}") log_event(f"cleanup invoked by {ctx.author}")
guild = bot.get_guild(GUILD_ID)
removed = [] removed = []
for discord_id, jf_username in get_accounts():
m = guild.get_member(discord_id) for discord_id, jf_username, jf_id, js_id in get_accounts():
if m is None or not has_required_role(m): member = None
for gid in GUILD_IDS:
guild = bot.get_guild(gid)
if guild:
member = guild.get_member(discord_id)
if member:
break
if member is None or not has_required_role(member):
if delete_jellyfin_user(jf_username): if delete_jellyfin_user(jf_username):
delete_account(discord_id) delete_account(discord_id)
if JELLYSEERR_ENABLED and js_id:
try:
headers = {"X-Api-Key": JELLYSEERR_API_KEY}
dr = requests.delete(f"{JELLYSEERR_URL}/api/v1/user/{js_id}", headers=headers, timeout=10)
if dr.status_code in (200, 204):
print(f"[Jellyseerr] User {js_id} removed successfully.")
except Exception as e:
print(f"[Jellyseerr] Failed to delete user {js_id}: {e}")
removed.append(jf_username) removed.append(jf_username)
log_channel = bot.get_channel(SYNC_LOG_CHANNEL_ID) log_channel = bot.get_channel(SYNC_LOG_CHANNEL_ID)
@@ -458,12 +646,59 @@ async def cleanup(ctx):
await ctx.send("✅ Cleanup complete.") await ctx.send("✅ Cleanup complete.")
@bot.command()
async def listvalidusers(ctx):
"""Admin-only: List how many registered users have a valid role."""
if not has_admin_role(ctx.author):
await ctx.send("❌ You dont have permission to use this command.")
return
accounts = get_accounts()
valid_users = []
invalid_users = []
for discord_id, jf_username, jf_id, js_id in accounts:
user = await bot.fetch_user(discord_id)
if has_required_role(user):
valid_users.append(user)
else:
invalid_users.append(user)
embed = discord.Embed(
title="📊 Registered User Role Status",
color=discord.Color.green()
)
embed.add_field(
name="✅ Valid Users",
value=f"{len(valid_users)} users",
inline=True
)
embed.add_field(
name="❌ Invalid Users",
value=f"{len(invalid_users)} users",
inline=True
)
if len(valid_users) > 0:
embed.add_field(
name="Valid Users List",
value="\n".join([u.mention for u in valid_users[:20]]) + ("..." if len(valid_users) > 20 else ""),
inline=False
)
if len(invalid_users) > 0:
embed.add_field(
name="Invalid Users List",
value="\n".join([u.mention for u in invalid_users[:20]]) + ("..." if len(invalid_users) > 20 else ""),
inline=False
)
await ctx.send(embed=embed)
@bot.command() @bot.command()
async def lastcleanup(ctx): async def lastcleanup(ctx):
log_event(f"lastcleanup invoked by {ctx.author}") log_event(f"lastcleanup invoked by {ctx.author}")
member = ctx.guild.get_member(ctx.author.id) member = ctx.guild.get_member(ctx.author.id)
if not has_admin_role(member): if not has_admin_role(ctx.author):
await ctx.send("❌ You dont have permission to view the last cleanup.") await ctx.send("❌ You dont have permission to view the last cleanup.")
return return
@@ -517,7 +752,7 @@ async def searchdiscord(ctx, user: discord.User = None):
async def scanlibraries(ctx): async def scanlibraries(ctx):
log_event(f"scanlibraries invoked by {ctx.author}") log_event(f"scanlibraries invoked by {ctx.author}")
member = ctx.guild.get_member(ctx.author.id) member = ctx.guild.get_member(ctx.author.id)
if not has_admin_role(member): if not has_admin_role(ctx.author):
await ctx.send("❌ You dont have permission to use this command.") await ctx.send("❌ You dont have permission to use this command.")
return return
@@ -576,7 +811,7 @@ async def setprefix(ctx, new_prefix: str = None):
return return
member = ctx.guild.get_member(ctx.author.id) member = ctx.guild.get_member(ctx.author.id)
if not member or not has_admin_role(member): if not has_admin_role(ctx.author):
await ctx.send("❌ You dont have permission to use this command.") await ctx.send("❌ You dont have permission to use this command.")
return return
@@ -603,7 +838,7 @@ async def setprefix(ctx, new_prefix: str = None):
async def updates(ctx): async def updates(ctx):
log_event(f"updates invoked by {ctx.author}") log_event(f"updates invoked by {ctx.author}")
member = ctx.guild.get_member(ctx.author.id) member = ctx.guild.get_member(ctx.author.id)
if not has_admin_role(member): if not has_admin_role(ctx.author):
await ctx.send("❌ You dont have permission to use this command.") await ctx.send("❌ You dont have permission to use this command.")
return return
@@ -621,7 +856,7 @@ async def updates(ctx):
async def logging(ctx, state: str): async def logging(ctx, state: str):
"""Admin-only: Enable or disable event logging.""" """Admin-only: Enable or disable event logging."""
member = ctx.guild.get_member(ctx.author.id) member = ctx.guild.get_member(ctx.author.id)
if not member or not has_admin_role(member): if not has_admin_role(ctx.author):
await ctx.send("❌ You dont have permission to use this command.") await ctx.send("❌ You dont have permission to use this command.")
return return
@@ -655,7 +890,7 @@ async def logging(ctx, state: str):
async def help_command(ctx): async def help_command(ctx):
log_event(f"Command help invoked by {ctx.author}") log_event(f"Command help invoked by {ctx.author}")
member = ctx.guild.get_member(ctx.author.id) member = ctx.guild.get_member(ctx.author.id)
is_admin = has_admin_role(member) is_admin = has_admin_role(ctx.author)
embed = discord.Embed( embed = discord.Embed(
title=f"📖 Jellyfin Bot Help {BOT_VERSION}", title=f"📖 Jellyfin Bot Help {BOT_VERSION}",
@@ -663,24 +898,39 @@ async def help_command(ctx):
color=discord.Color.blue() color=discord.Color.blue()
) )
embed.add_field(name="User Commands", value=( # User commands
f"`{PREFIX}createaccount <username> <password>` - Create your Jellyfin account\n" user_cmds = [
f"`{PREFIX}recoveraccount <newpassword>` - Reset your password\n" f"`{PREFIX}createaccount <username> <password>` - Create your Jellyfin account",
f"`{PREFIX}deleteaccount <username>` - Delete your Jellyfin account\n" f"`{PREFIX}recoveraccount <newpassword>` - Reset your password",
), inline=False) f"`{PREFIX}deleteaccount <username>` - Delete your Jellyfin account"
]
# Only show trialaccount if enabled
if ENABLE_TRIAL_ACCOUNTS:
user_cmds.append(f"`{PREFIX}trialaccount <username> <password>` - Create a 24-hour trial Jellyfin account")
embed.add_field(name="User Commands", value="\n".join(user_cmds), inline=False)
# Admin commands
if is_admin: if is_admin:
# Dynamic link command line
link_command = f"`{PREFIX}link <jellyfin_username> @user` - Manually link accounts"
if JELLYSEERR_ENABLED:
link_command = f"`{PREFIX}link <jellyfin_username> @user <Jellyseerr ID>` - Manually link accounts with Jellyseerr"
embed.add_field(name="Admin Commands", value=( embed.add_field(name="Admin Commands", value=(
f"`{PREFIX}cleanup` - Remove Jellyfin accounts from users without roles\n" f"`{PREFIX}cleanup` - Remove Jellyfin accounts from users without roles\n"
f"`{PREFIX}listvalidusers` - Show number of valid and invalid accounts\n"
f"`{PREFIX}lastcleanup` - See Last cleanup time, and time remaining before next cleanup\n" f"`{PREFIX}lastcleanup` - See Last cleanup time, and time remaining before next cleanup\n"
f"`{PREFIX}searchaccount <jellyfin_username>` - Find linked Discord user\n" f"`{PREFIX}searchaccount <jellyfin_username>` - Find linked Discord user\n"
f"`{PREFIX}searchdiscord @user` - Find linked Jellyfin account\n" f"`{PREFIX}searchdiscord @user` - Find linked Jellyfin account\n"
f"`{PREFIX}scanlibraries` - Scan all Jellyfin libraries\n" f"`{PREFIX}scanlibraries` - Scan all Jellyfin libraries\n"
f"`{PREFIX}link <jellyfin_username> @user` - Manually link accounts\n" f"{link_command}\n"
f"`{PREFIX}unlink @user` - Manually unlink accounts\n" f"`{PREFIX}unlink @user` - Manually unlink accounts\n"
), inline=False) ), inline=False)
embed.add_field(name="Admin Bot Commands", value=( embed.add_field(name="Admin Bot Commands", value=(
f"`{PREFIX}setprefix` - Change the bots command prefix\n" f"`{PREFIX}setprefix` - Change the bot's command prefix\n"
f"`{PREFIX}updates` - Manually check for bot updates\n" f"`{PREFIX}updates` - Manually check for bot updates\n"
f"`{PREFIX}logging` - Enable/Disable Console Event Logging\n" f"`{PREFIX}logging` - Enable/Disable Console Event Logging\n"
), inline=False) ), inline=False)
@@ -694,22 +944,125 @@ import datetime
@tasks.loop(hours=24) @tasks.loop(hours=24)
async def daily_check(): async def daily_check():
guild = bot.get_guild(GUILD_ID) log_event("Running daily account cleanup check...")
removed = [] removed = []
for discord_id, jf_username in get_accounts(): # Normal accounts cleanup
m = guild.get_member(discord_id) for row in get_accounts():
if m is None or not has_required_role(m): # safe unpacking in case schema varies
discord_id = row[0]
jf_username = row[1] if len(row) > 1 else None
jf_id = row[2] if len(row) > 2 else None
js_id = row[3] if len(row) > 3 else None
# find the member across configured guilds
member = None
for gid in GUILD_IDS:
guild = bot.get_guild(gid)
if not guild:
continue
candidate = guild.get_member(discord_id)
if candidate:
member = candidate
break
# if no member found or member doesn't have a required role -> delete account
if member is None or not has_required_role(member):
if jf_username:
try:
if delete_jellyfin_user(jf_username): if delete_jellyfin_user(jf_username):
log_event(f"Deleted Jellyfin user {jf_username} for Discord ID {discord_id}")
else:
log_event(f"Failed to delete Jellyfin user {jf_username} for Discord ID {discord_id}")
except Exception as e:
print(f"[Cleanup] Error deleting Jellyfin user {jf_username}: {e}")
# remove DB entry for normal account
try:
delete_account(discord_id) delete_account(discord_id)
removed.append(jf_username) except Exception as e:
print(f"[Cleanup] Error removing DB entry for Discord ID {discord_id}: {e}")
if removed: # remove from Jellyseerr if we have an id and integration enabled
print(f"Daily cleanup: removed {len(removed)} accounts: {removed}") if JELLYSEERR_ENABLED and js_id:
try:
if delete_jellyseerr_user(js_id):
log_event(f"Deleted Jellyseerr user {js_id} for Discord ID {discord_id}")
else:
log_event(f"Failed to delete Jellyseerr user {js_id} for Discord ID {discord_id}")
except Exception as e:
print(f"[Cleanup] Failed to delete Jellyseerr user {js_id}: {e}")
# Log last run timestamp removed.append(jf_username or f"{discord_id}")
# Trial accounts cleanup (persistent history table)
try:
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor(dictionary=True)
cur.execute("SELECT * FROM trial_accounts WHERE expired=0")
trials = cur.fetchall()
for trial in trials:
created_at = trial.get("trial_created_at") or trial.get("created_at") # compatibility
if not created_at:
continue
# created_at is a datetime from the DB (cursor dictionary=True)
if datetime.datetime.utcnow() > created_at + datetime.timedelta(hours=24):
# delete from Jellyfin (best-effort)
try:
delete_jellyfin_user(trial.get("jellyfin_username"))
except Exception as e:
print(f"[Trial Cleanup] Error deleting trial Jellyfin user {trial.get('jellyfin_username')}: {e}")
# mark trial as expired
try:
cur.execute("UPDATE trial_accounts SET expired=1 WHERE discord_id=%s", (trial["discord_id"],))
conn.commit()
except Exception as e:
print(f"[Trial Cleanup] Error marking trial expired for {trial['discord_id']}: {e}")
removed.append(f"{trial.get('jellyfin_username')} (trial)")
except Exception as e:
print(f"[Trial Cleanup] Error reading trial accounts: {e}")
finally:
try:
cur.close()
conn.close()
except Exception:
pass
# record last run in metadata and cleanup_logs
try:
set_metadata("last_cleanup", datetime.datetime.utcnow().isoformat()) set_metadata("last_cleanup", datetime.datetime.utcnow().isoformat())
log_event(f"Daily cleanup: removed {len(removed)} accounts: {removed}") except Exception as e:
print(f"[Cleanup] Failed to set last_cleanup metadata: {e}")
try:
conn = mysql.connector.connect(
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
)
cur = conn.cursor()
cur.execute("INSERT INTO cleanup_logs (run_at) VALUES (%s)", (datetime.datetime.utcnow(),))
conn.commit()
cur.close()
conn.close()
except Exception as e:
print(f"[Cleanup] Failed to insert cleanup_logs: {e}")
# post results to sync channel if anything removed
if removed:
msg = f"🧹 Removed {len(removed)} Jellyfin accounts: {', '.join(removed)}"
print(msg)
try:
log_channel = bot.get_channel(SYNC_LOG_CHANNEL_ID)
if log_channel:
await log_channel.send(msg)
except Exception as e:
print(f"[Cleanup] Failed to send removed message to sync channel: {e}")
@tasks.loop(hours=1) @tasks.loop(hours=1)
async def check_for_updates(): async def check_for_updates():
@@ -731,8 +1084,6 @@ async def check_for_updates():
print(f"[Update Check] Failed: {e}") print(f"[Update Check] Failed: {e}")
@bot.event @bot.event
async def on_ready(): async def on_ready():
print(f"Logged in as {bot.user}") print(f"Logged in as {bot.user}")

1
version.json Normal file
View File

@@ -0,0 +1 @@
{ "version": "1.0.4" }

View File

@@ -1 +1 @@
1.0.2 1.0.4