3 Commits
1.0.0 ... 1.0.1

Author SHA1 Message Date
308d57b1f8 Update version.txt 2025-09-06 09:13:06 -05:00
aefd8780b8 Added Jellyseerr Support 2025-09-06 09:12:56 -05:00
483a66ecd1 Update README.md 2025-09-06 08:12:39 -05:00
4 changed files with 162 additions and 30 deletions

5
.env
View File

@@ -9,6 +9,11 @@ REQUIRED_ROLE_IDS=333333333333333333,444444444444444444
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
# Jellyseerr
JELLYSEERR_ENABLED=true
JELLYSEERR_URL=http://localhost:5055
JELLYSEERR_API_KEY=your_api_key_here
# MySQL # MySQL
DB_HOST=localhost DB_HOST=localhost
DB_USER=root DB_USER=root

View File

@@ -16,6 +16,7 @@ Fill out values in the .env and you're good to go!
- Run Library Scanning - Run Library Scanning
- Manual Account Linking (For previously made Jellyfin accounts) - Manual Account Linking (For previously made Jellyfin accounts)
- Change bot prefix live - Change bot prefix live
- Checks for new releases
# Command Overview # Command Overview
@@ -53,3 +54,4 @@ Fill out values in the .env and you're good to go!
***Admin Bot Commands*** ***Admin Bot Commands***
- `!setprefix` - Change the bots command prefix - `!setprefix` - Change the bots command prefix
- `!updates` - Manually check for bot updates

177
app.py
View File

@@ -30,12 +30,16 @@ 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")
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_HOST = get_env_var("DB_HOST")
DB_USER = get_env_var("DB_USER") 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.0" BOT_VERSION = "1.0.1"
VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellyfin-Discord/main/version.txt" VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellyfin-Discord/main/version.txt"
RELEASES_URL = "https://github.com/PenguCCN/Jellyfin-Discord/releases" 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 # DATABASE SETUP
# ===================== # =====================
def init_db(): def init_db():
# Existing DB creation # 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()
conn = mysql.connector.connect( # Connect to the database
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME conn = mysql.connector.connect(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
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,
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(""" 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,
value VARCHAR(255) NOT NULL value VARCHAR(255) NOT NULL
) )
""") """)
conn.commit() conn.commit()
cur.close() cur.close()
conn.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( 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()
cur.execute("REPLACE INTO accounts (discord_id, jellyfin_username) VALUES (%s, %s)", cur.execute(
(discord_id, jellyfin_username)) "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() conn.commit()
cur.close() cur.close()
conn.close() conn.close()
def get_accounts(): def get_accounts():
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
@@ -120,11 +144,15 @@ def get_account_by_discord(discord_id):
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()
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() row = cur.fetchone()
cur.close() cur.close()
conn.close() conn.close()
return row return row # (jellyfin_username, jellyfin_id, jellyseerr_id)
def delete_account(discord_id): def delete_account(discord_id):
conn = mysql.connector.connect( 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) response = requests.post(f"{JELLYFIN_URL}/Users/{user_id}/Password", headers=headers, json=data)
return response.status_code in (200, 204) 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 # DISCORD HELPERS
# ===================== # =====================
@@ -233,27 +308,57 @@ async def on_message(message):
# ===================== # =====================
@bot.command() @bot.command()
async def createaccount(ctx, username: str, password: str): async def createaccount(ctx, username: str, password: str):
# DM-only
if not isinstance(ctx.channel, discord.DMChannel): if not isinstance(ctx.channel, discord.DMChannel):
try:
await ctx.message.delete() await ctx.message.delete()
await ctx.send(f"{ctx.author.mention} Please DM me to create your Jellyfin account.") except discord.Forbidden:
pass
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to create your Jellyfin account.")
return return
guild = bot.get_guild(GUILD_ID) 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): if not member or not has_required_role(member):
await ctx.send("You dont have the required role to create an account.") await ctx.send(f"{ctx.author.mention}, you dont have the required role.")
return return
if get_account_by_discord(ctx.author.id): 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 return
# Create Jellyfin user
if create_jellyfin_user(username, password): if create_jellyfin_user(username, password):
add_account(ctx.author.id, username) jf_id = get_jellyfin_user(username)
await ctx.send(f"✅ Account created! You can log in at {JELLYFIN_URL}") 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: else:
await ctx.send("❌ Failed to create account. Username may already exist.") 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(f"❌ Failed to create Jellyfin account **{username}**. It may already exist.")
@bot.command() @bot.command()
async def recoveraccount(ctx, new_password: str): async def recoveraccount(ctx, new_password: str):
@@ -284,20 +389,40 @@ async def recoveraccount(ctx, new_password: str):
@bot.command() @bot.command()
async def deleteaccount(ctx, username: str): async def deleteaccount(ctx, username: str):
if not isinstance(ctx.channel, discord.DMChannel): if not isinstance(ctx.channel, discord.DMChannel):
try:
await ctx.message.delete() await ctx.message.delete()
await ctx.send(f"{ctx.author.mention} Please DM me to delete your Jellyfin account.") except discord.Forbidden:
pass
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to delete your Jellyfin account.")
return return
# Fetch account linked to this Discord user
acc = get_account_by_discord(ctx.author.id) acc = get_account_by_discord(ctx.author.id)
if not acc or acc[0].lower() != username.lower(): 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 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): if delete_jellyfin_user(username):
delete_account(ctx.author.id) 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: else:
await ctx.send("❌ Failed to delete account.") await ctx.send(f"❌ Failed to delete Jellyfin account **{username}**.")
@bot.command() @bot.command()
async def cleanup(ctx): async def cleanup(ctx):

View File

@@ -1 +1 @@
1.0.0 1.0.1