From aefd8780b8966c73b70dd5e7ac15b0cde561b792 Mon Sep 17 00:00:00 2001 From: Pengu Date: Sat, 6 Sep 2025 09:12:56 -0500 Subject: [PATCH] Added Jellyseerr Support --- .env | 5 ++ app.py | 181 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 158 insertions(+), 28 deletions(-) diff --git a/.env b/.env index 0501233..687bf61 100644 --- a/.env +++ b/.env @@ -9,6 +9,11 @@ REQUIRED_ROLE_IDS=333333333333333333,444444444444444444 JELLYFIN_URL=http://127.0.0.1:8096 JELLYFIN_API_KEY=your_jellyfin_api_key +# Jellyseerr +JELLYSEERR_ENABLED=true +JELLYSEERR_URL=http://localhost:5055 +JELLYSEERR_API_KEY=your_api_key_here + # MySQL DB_HOST=localhost DB_USER=root diff --git a/app.py b/app.py index 29f923c..cd8a7a7 100644 --- a/app.py +++ b/app.py @@ -30,12 +30,16 @@ 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") +JELLYSEERR_ENABLED = os.getenv("JELLYSEERR_ENABLED", "false").lower() == "true" +JELLYSEERR_URL = os.getenv("JELLYSEERR_URL", "").rstrip("/") +JELLYSEERR_API_KEY = os.getenv("JELLYSEERR_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") -BOT_VERSION = "1.0.0" +BOT_VERSION = "1.0.1" VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellyfin-Discord/main/version.txt" RELEASES_URL = "https://github.com/PenguCCN/Jellyfin-Discord/releases" @@ -51,48 +55,68 @@ bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None) # DATABASE SETUP # ===================== def init_db(): - # Existing DB creation - conn = mysql.connector.connect( - host=DB_HOST, user=DB_USER, password=DB_PASSWORD - ) + # Create database if it doesn't exist + 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 - ) + # Connect to the database + conn = mysql.connector.connect(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME) cur = conn.cursor() + + # Create accounts table if it doesn't exist cur.execute(""" CREATE TABLE IF NOT EXISTS accounts ( discord_id BIGINT PRIMARY KEY, - jellyfin_username VARCHAR(255) NOT NULL + jellyfin_username VARCHAR(255) NOT NULL, + jellyfin_id VARCHAR(255) NOT NULL, + jellyseerr_id VARCHAR(255) DEFAULT NULL ) """) - # New table for metadata + + # Ensure jellyfin_id exists + cur.execute("SHOW COLUMNS FROM accounts LIKE 'jellyfin_id'") + if cur.fetchone() is None: + cur.execute("ALTER TABLE accounts ADD COLUMN jellyfin_id VARCHAR(255) NOT NULL") + print("[DB] Added missing column 'jellyfin_id' to accounts table.") + + # Ensure jellyseerr_id exists + cur.execute("SHOW COLUMNS FROM accounts LIKE 'jellyseerr_id'") + if cur.fetchone() is None: + cur.execute("ALTER TABLE accounts ADD COLUMN jellyseerr_id VARCHAR(255) DEFAULT NULL") + print("[DB] Added missing column 'jellyseerr_id' to accounts table.") + + # Create bot_metadata table if it doesn't exist cur.execute(""" CREATE TABLE IF NOT EXISTS bot_metadata ( key_name VARCHAR(255) PRIMARY KEY, value VARCHAR(255) NOT NULL ) """) + conn.commit() cur.close() conn.close() -def add_account(discord_id, jellyfin_username): + +def add_account(discord_id, username, jf_id, js_id=None): 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)) + cur.execute( + "REPLACE INTO accounts (discord_id, jellyfin_username, jellyfin_id, jellyseerr_id) VALUES (%s, %s, %s, %s)", + (discord_id, username, jf_id, js_id) + ) 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 @@ -120,11 +144,15 @@ def get_account_by_discord(discord_id): 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,)) + cur.execute( + "SELECT jellyfin_username, jellyfin_id, jellyseerr_id FROM accounts WHERE discord_id=%s", + (discord_id,) + ) row = cur.fetchone() cur.close() conn.close() - return row + return row # (jellyfin_username, jellyfin_id, jellyseerr_id) + def delete_account(discord_id): conn = mysql.connector.connect( @@ -171,6 +199,53 @@ def reset_jellyfin_password(username: str, new_password: str) -> bool: response = requests.post(f"{JELLYFIN_URL}/Users/{user_id}/Password", headers=headers, json=data) return response.status_code in (200, 204) +# ===================== +# JELLYSEERR HELPERS +# ===================== + +def import_jellyseerr_user(jellyfin_user_id: str) -> str: + """Import user into Jellyseerr. Returns the Jellyseerr user ID if successful, else None.""" + if not JELLYSEERR_ENABLED: + return None + headers = {"X-Api-Key": JELLYSEERR_API_KEY, "Content-Type": "application/json"} + data = {"jellyfinUserIds": [jellyfin_user_id]} + try: + url = f"{JELLYSEERR_URL}/api/v1/user/import-from-jellyfin" + r = requests.post(url, headers=headers, json=data, timeout=15) + if r.status_code in (200, 201): + js_user = r.json() + if isinstance(js_user, list) and len(js_user) > 0 and "id" in js_user[0]: + js_id = js_user[0]["id"] + print(f"[Jellyseerr] User {jellyfin_user_id} imported successfully with Jellyseerr ID {js_id}.") + return js_id + print(f"[Jellyseerr] Import failed. Status: {r.status_code}, Response: {r.text}") + return None + except Exception as e: + print(f"[Jellyseerr] Failed to import user: {e}") + return None + + + +def delete_jellyseerr_user(username: str) -> bool: + if not JELLYSEERR_ENABLED: + return True + headers = {"X-Api-Key": JELLYSEERR_API_KEY} + try: + # First fetch users to find matching ID + r = requests.get(f"{JELLYSEERR_URL}/api/v1/user", headers=headers, timeout=10) + if r.status_code != 200: + return False + users = r.json() + for u in users: + if u.get("username", "").lower() == username.lower(): + user_id = u["id"] + dr = requests.delete(f"{JELLYSEERR_URL}/api/v1/user/{user_id}", headers=headers, timeout=10) + return dr.status_code in (200, 204) + return True # no user found, nothing to delete + except Exception as e: + print(f"[Jellyseerr] Failed to delete user {username}: {e}") + return False + # ===================== # DISCORD HELPERS # ===================== @@ -233,27 +308,57 @@ async def on_message(message): # ===================== @bot.command() async def createaccount(ctx, username: str, password: str): + # DM-only 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.") + try: + await ctx.message.delete() + except discord.Forbidden: + pass + 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) + member = guild.get_member(ctx.author.id) if guild else None if not member or not has_required_role(member): - await ctx.send("❌ You don’t have the required role to create an account.") + await ctx.send(f"❌ {ctx.author.mention}, you don’t have the required role.") return if get_account_by_discord(ctx.author.id): - await ctx.send("❌ You already have a Jellyfin account.") + await ctx.send(f"❌ {ctx.author.mention}, you already have a Jellyfin account.") return + # Create Jellyfin user 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}") + 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 + + js_id = None + # Import to Jellyseerr if enabled + if JELLYSEERR_ENABLED: + js_id = import_jellyseerr_user(jf_id) + + # Store account in DB + add_account(ctx.author.id, username, jf_id, js_id) + + if JELLYSEERR_ENABLED: + if js_id: + await ctx.send( + f"✅ Jellyfin account **{username}** created and imported into Jellyseerr!\n" + f"🌐 Login here: {JELLYFIN_URL}" + ) + else: + await ctx.send( + f"⚠️ Jellyfin account **{username}** created, but Jellyseerr import failed.\n" + f"🌐 Login here: {JELLYFIN_URL}" + ) + else: + await ctx.send(f"✅ Jellyfin account **{username}** created!\n🌐 Login here: {JELLYFIN_URL}") else: - await ctx.send("❌ Failed to create account. Username may already exist.") + await ctx.send(f"❌ Failed to create Jellyfin account **{username}**. It may already exist.") + @bot.command() async def recoveraccount(ctx, new_password: str): @@ -284,20 +389,40 @@ async def recoveraccount(ctx, new_password: str): @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.") + try: + await ctx.message.delete() + except discord.Forbidden: + pass + await ctx.send(f"{ctx.author.mention} ❌ Please DM me to delete your Jellyfin account.") return + # Fetch account linked to this Discord user 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.") + await ctx.send(f"❌ {ctx.author.mention}, that Jellyfin account is not linked to you.") return + jf_id = acc[1] # Jellyfin ID + js_id = acc[2] if len(acc) > 2 else None # Jellyseerr ID + + # Delete Jellyfin account if delete_jellyfin_user(username): delete_account(ctx.author.id) - await ctx.send("✅ Account deleted.") + + # Delete Jellyseerr user if enabled + 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}") + + await ctx.send(f"✅ Jellyfin account **{username}** deleted successfully.") else: - await ctx.send("❌ Failed to delete account.") + await ctx.send(f"❌ Failed to delete Jellyfin account **{username}**.") + @bot.command() async def cleanup(ctx):