@@ -30,15 +30,28 @@ 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.2 "
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 "
# =====================
# EVENT LOGGING
# =====================
EVENT_LOGGING = os . getenv ( " EVENT_LOGGING " , " false " ) . lower ( ) == " true "
def log_event ( message : str ) :
if EVENT_LOGGING :
print ( f " [EVENT] { datetime . datetime . utcnow ( ) . isoformat ( ) } | { message } " )
# =====================
# =====================
# DISCORD SETUP
# DISCORD SETUP
# =====================
# =====================
@@ -51,80 +64,106 @@ bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None)
# DATABASE SETUP
# DATABASE SETUP
# =====================
# =====================
def init_db ( ) :
def init_db ( ) :
# Existing DB creation
log_event ( f " Initiating Database... " )
conn = mysql . connector . connect (
# Create database if it doesn't exist
host = DB_HOST , user = DB_USER , password = DB_PASSWORD
conn = mysql . connector . connect ( 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
)
)
cur = conn . cursor ( )
cur = conn . cursor ( )
cur . execute ( " SELECT discord_id, jellyfin_username FROM accounts " )
cur . execute ( " SELECT discord_id, jellyfin_username, jellyfin_id, jellyseerr_id FROM accounts " )
rows = cur . fetchall ( )
rows = cur . fetchall ( )
cur . close ( )
cur . close ( )
conn . close ( )
conn . close ( )
return rows
return rows
def get_account_by_jellyfin ( username ) :
def get_account_by_jellyfin ( username ) :
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 ( " SELECT discord_id FROM accounts WHERE jellyfin_username= %s " , ( username , ) )
cur . execute ( " SELECT discord_id, jellyfin_id, jellyseerr_id FROM accounts WHERE jellyfin_username= %s " , ( username , ) )
row = cur . fetchone ( )
row = cur . fetchone ( )
cur . close ( )
cur . close ( )
conn . close ( )
conn . close ( )
return row
return row # (discord_id, jf_id, js_id)
def get_account_by_discord ( discord_id ) :
def get_account_by_discord ( discord_id ) :
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 ( " 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, jf_id, js_id)
def delete_account ( discord_id ) :
def delete_account ( discord_id ) :
conn = mysql . connector . connect (
conn = mysql . connector . connect (
@@ -171,6 +210,62 @@ 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 get_jellyseerr_id ( jf_id : str ) - > str | None :
""" Return the Jellyseerr user ID for a given Jellyfin user ID. """
if not JELLYSEERR_ENABLED :
return None
headers = { " X-Api-Key " : JELLYSEERR_API_KEY }
try :
r = requests . get ( f " { JELLYSEERR_URL } /api/v1/user " , headers = headers , timeout = 10 )
if r . status_code != 200 :
return None
users = r . json ( )
for user in users :
if " jellyfinUserIds " in user and jf_id in user [ " jellyfinUserIds " ] :
return user [ " id " ]
return None
except Exception as e :
print ( f " [Jellyseerr] Failed to fetch user ID for Jellyfin ID { jf_id } : { e } " )
return None
def delete_jellyseerr_user ( js_id : str ) - > bool :
if not JELLYSEERR_ENABLED or not js_id :
return True
headers = { " X-Api-Key " : JELLYSEERR_API_KEY }
try :
dr = requests . delete ( f " { JELLYSEERR_URL } /api/v1/user/ { js_id } " , headers = headers , timeout = 10 )
return dr . status_code in ( 200 , 204 )
except Exception as e :
print ( f " [Jellyseerr] Failed to delete user { js_id } : { e } " )
return False
# =====================
# =====================
# DISCORD HELPERS
# DISCORD HELPERS
# =====================
# =====================
@@ -231,76 +326,123 @@ async def on_message(message):
# =====================
# =====================
# COMMANDS
# COMMANDS
# =====================
# =====================
def command_usage ( base : str , args : list [ str ] ) - > str :
""" Return usage message for a command. """
return f " ❌ Usage: ` { base } { ' ' . join ( args ) } ` "
def log_event ( message : str ) :
""" Log events to console if enabled in .env. """
if os . getenv ( " EVENT_LOGGING " , " false " ) . lower ( ) == " true " :
print ( f " [EVENT] { message } " )
@bot.command ( )
@bot.command ( )
async def createaccount ( ctx , username : str , password : str ) :
async def createaccount ( ctx , username : str = None , password : str = None ) :
log_event ( f " createaccount invoked by { ctx . author } " )
if username is None or password is None :
await ctx . send ( command_usage ( f " { PREFIX } createaccount " , [ " <username> " , " <password> " ] ) )
return
if not isinstance ( ctx . channel , discord . DMChannel ) :
if not isinstance ( ctx . channel , discord . DMChannel ) :
await ctx . message . delete ( )
try : 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 ( " ❌ Y ou don’ t have the required role to create an account . " )
await ctx . send ( f " ❌ { ctx . author . mention } , y ou don’ t 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 ( " ❌ Y ou already have a Jellyfin account. " )
await ctx . send ( f " ❌ { ctx . author . mention } , y ou already have a Jellyfin account. " )
return
return
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
if JELLYSEERR_ENABLED :
js_id = import_jellyseerr_user ( jf_id )
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 🌐 Login here: { JELLYFIN_URL } " )
else :
await ctx . send ( f " ⚠️ Jellyfin account ** { username } ** created, but Jellyseerr import failed. \n 🌐 Login here: { JELLYFIN_URL } " )
else :
await ctx . send ( f " ✅ Jellyfin account ** { username } ** created! \n 🌐 Login here: { JELLYFIN_URL } " )
else :
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 ( )
@bot.command ( )
async def recoveraccount ( ctx , new_password : str ) :
async def recoveraccount ( ctx , new_password : str = None ) :
""" DM-only: reset your Jellyfin password """
log_event ( f " recoveraccount invoked by { ctx . author } " )
# Ensure it's a DM
if new_password is None :
await ctx . send ( command_usage ( f " { PREFIX } recoveraccount " , [ " <newpassword> " ] ) )
return
if not isinstance ( ctx . channel , discord . DMChannel ) :
if not isinstance ( ctx . channel , discord . DMChannel ) :
await ctx . message . delete ( )
await ctx . message . delete ( )
await ctx . send ( f " { ctx . author . mention } Please DM me to reset your password. " )
await ctx . send ( f " { ctx . author . mention } Please DM me to reset your password. " )
return
return
# Fetch the Jellyfin 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 :
if not acc :
await ctx . send ( " ❌ You do not have a linked Jellyfin account. " )
await ctx . send ( " ❌ You do not have a linked Jellyfin account. " )
return
return
username = acc [ 0 ] # the Jellyfin username
username = acc [ 0 ]
# Reset the password
if reset_jellyfin_password ( username , new_password ) :
if reset_jellyfin_password ( username , new_password ) :
await ctx . send (
await ctx . send ( f " ✅ Your Jellyfin password for ** { username } ** has been reset! \n 🌐 Login here: { JELLYFIN_URL } " )
f " ✅ Your Jellyfin password for ** { username } ** has been reset! \n "
f " 🌐 Login here: { JELLYFIN_URL } "
)
else :
else :
await ctx . send ( f " ❌ Failed to reset password for ** { username } **. Please contact an admin. " )
await ctx . send ( f " ❌ Failed to reset password for ** { username } **. Please contact an admin. " )
@bot.command ( )
@bot.command ( )
async def deleteaccount ( ctx , username : str ) :
async def deleteaccount ( ctx , username : str = None ) :
log_event ( f " deleteaccount invoked by { ctx . author } " )
if username is None :
await ctx . send ( command_usage ( f " { PREFIX } deleteaccount " , [ " <username> " ] ) )
return
if not isinstance ( ctx . channel , discord . DMChannel ) :
if not isinstance ( ctx . channel , discord . DMChannel ) :
await ctx . message . delete ( )
try : 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
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 ( " ❌ T hat Jellyfin account is not linked to your Discord user . " )
await ctx . send ( f " ❌ { ctx . author . mention } , t hat Jellyfin account is not linked to you. " )
return
return
jf_id , js_id = acc [ 1 ] , acc [ 2 ] if len ( acc ) > 2 else None
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. " )
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 ) :
log_event ( f " cleanup invoked by { ctx . author } " )
guild = bot . get_guild ( GUILD_ID )
guild = bot . get_guild ( GUILD_ID )
removed = [ ]
removed = [ ]
for discord_id , jf_username in get_accounts ( ) :
for discord_id , jf_username in get_accounts ( ) :
@@ -316,8 +458,10 @@ async def cleanup(ctx):
await ctx . send ( " ✅ Cleanup complete. " )
await ctx . send ( " ✅ Cleanup complete. " )
@bot.command ( )
@bot.command ( )
async def lastcleanup ( ctx ) :
async def lastcleanup ( ctx ) :
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 ( member ) :
await ctx . send ( " ❌ You don’ t have permission to view the last cleanup. " )
await ctx . send ( " ❌ You don’ t have permission to view the last cleanup. " )
@@ -336,17 +480,14 @@ async def lastcleanup(ctx):
hours , remainder = divmod ( int ( time_remaining . total_seconds ( ) ) , 3600 )
hours , remainder = divmod ( int ( time_remaining . total_seconds ( ) ) , 3600 )
minutes , seconds = divmod ( remainder , 60 )
minutes , seconds = divmod ( remainder , 60 )
await ctx . send (
await ctx . send ( f " 🧹 Last cleanup ran at ** { last_run_dt . strftime ( ' % Y- % m- %d % H: % M: % S ' ) } UTC** \n ⏳ Time until next cleanup: { hours } h { minutes } m { seconds } s " )
f " 🧹 Last cleanup ran at ** { last_run_dt . strftime ( ' % Y- % m- %d % H: % M: % S ' ) } UTC** \n "
f " ⏳ Time until next cleanup: { hours } h { minutes } m { seconds } s "
)
@bot.command ( )
@bot.command ( )
async def searchaccount ( ctx , username : str ) :
async def searchaccount ( ctx , username : str = None ) :
member = ctx . guild . get_member ( ctx . author . id )
log_event ( f " searchaccount invoked by { ctx . author } " )
if not has_admin_role ( member ) :
if username is None :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
await ctx . send ( command_usage ( f " { PREFIX } searchaccount " , [ " <jellyfin_username> " ] ) )
return
return
result = get_account_by_jellyfin ( username )
result = get_account_by_jellyfin ( username )
@@ -357,11 +498,12 @@ async def searchaccount(ctx, username: str):
else :
else :
await ctx . send ( " ❌ No linked Discord user found for that Jellyfin account. " )
await ctx . send ( " ❌ No linked Discord user found for that Jellyfin account. " )
@bot.command ( )
@bot.command ( )
async def searchdiscord ( ctx , user : discord . User ) :
async def searchdiscord ( ctx , user : discord . User = None ) :
member = ctx . guild . get_member ( ctx . author . id )
log_event ( f " searchdiscord invoked by { ctx . author } " )
if not has_admin_role ( member ) :
if user is None :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
await ctx . send ( command_usage ( f " { PREFIX } searchdiscord " , [ " @user " ] ) )
return
return
result = get_account_by_discord ( user . id )
result = get_account_by_discord ( user . id )
@@ -370,52 +512,69 @@ async def searchdiscord(ctx, user: discord.User):
else :
else :
await ctx . send ( " ❌ That Discord user does not have a linked Jellyfin account. " )
await ctx . send ( " ❌ That Discord user does not have a linked Jellyfin account. " )
@bot.command ( )
@bot.command ( )
async def scanlibraries ( ctx ) :
async def scanlibraries ( ctx ) :
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 ( member ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
return
headers = { " X-Emby-Token " : JELLYFIN_API_KEY }
response = requests . post ( f " { JELLYFIN_URL } /Library/Refresh " , headers= { " X-Emby-Token " : JELLYFIN_API_KEY } )
response = requests . post ( f " { JELLYFIN_URL } /Library/Refresh " , headers = headers )
if response . status_code in ( 200 , 204 ) :
if response . status_code in ( 200 , 204 ) :
await ctx . send ( " ✅ All Jellyfin libraries are being scanned. " )
await ctx . send ( " ✅ All Jellyfin libraries are being scanned. " )
else :
else :
await ctx . send ( f " ❌ Failed to start library scan. Status code: { response . status_code } " )
await ctx . send ( f " ❌ Failed to start library scan. Status code: { response . status_code } " )
@bot.command ( )
@bot.command ( )
async def link ( ctx , jellyfin_username : str , user : discord . User ) :
async def link ( ctx , jellyfin_username : str = None , user : discord . User = None , js_id : str = None ) :
member = ctx . guild . get_member ( ctx . author . id )
log_event ( f " link invoked by { ctx . author } " )
if not has_admin_role ( member ) :
usage_args = [ " <Jellyfin Account> " , " <@user> " ]
await ctx . s end( " ❌ You don’ t have permission to use this command. " )
if JELLYSEERR_ENABLED : usage_args . app end( " <Jellyseerr ID> " )
if jellyfin_username is None or user is None or ( JELLYSEERR_ENABLED and js_id is None ) :
await ctx . send ( command_usage ( f " { PREFIX } link " , usage_args ) )
return
return
add_account ( user . id , jellyfin_username )
existing_acc = get_account_by_discord ( user . id )
if existing_acc :
await ctx . send ( f " ❌ Discord user { user . mention } already has a linked account. " )
return
jf_id = get_jellyfin_user ( jellyfin_username )
if not jf_id :
await ctx . send ( f " ❌ Could not find Jellyfin account ** { jellyfin_username } **. Make sure it exists. " )
return
add_account ( user . id , jellyfin_username , jf_id , js_id )
await ctx . send ( f " ✅ Linked Jellyfin account ** { jellyfin_username } ** to { user . mention } . " )
await ctx . send ( f " ✅ Linked Jellyfin account ** { jellyfin_username } ** to { user . mention } . " )
@bot.command ( )
async def unlink ( ctx , discord_user : discord . User ) :
""" Admin-only: unlink a Jellyfin account from a Discord user (without deleting the account) """
guild = ctx . guild
member = guild . get_member ( ctx . author . id ) if guild else None
if not member or not has_admin_role ( member ) :
@bot.command ( )
await ctx . send ( f " ❌ { ctx . author . mention } , you don’ t have permission to use this command. " )
async def unlink ( ctx , discord_user : discord . User = None ) :
log_event ( f " unlink invoked by { ctx . author } " )
if discord_user is None :
await ctx . send ( command_usage ( f " { PREFIX } unlink " , [ " @user " ] ) )
return
return
# Check if the Discord user has a linked Jellyfin account
account = get_account_by_discord ( discord_user . id )
account = get_account_by_discord ( discord_user . id )
if not account :
if not account :
await ctx . send ( f " ❌ Discord user { discord_user . mention } does not have a linked Jellyfin account. " )
await ctx . send ( f " ❌ Discord user { discord_user . mention } does not have a linked Jellyfin account. " )
return
return
# Remove the database entry
delete_account ( discord_user . id )
delete_account ( discord_user . id )
await ctx . send ( f " ✅ Unlinked Jellyfin account ** { account [ 0 ] } ** from Discord user { discord_user . mention } . " )
await ctx . send ( f " ✅ Unlinked Jellyfin account ** { account [ 0 ] } ** from Discord user { discord_user . mention } . " )
@bot.command ( )
@bot.command ( )
async def setprefix ( ctx , new_prefix : str ) :
async def setprefix ( ctx , new_prefix : str = None ) :
log_event ( f " setprefix invoked by { ctx . author } " )
if new_prefix is None :
await ctx . send ( command_usage ( f " { PREFIX } setprefix " , [ " <symbol> " ] ) )
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 member or not has_admin_role ( member ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
@@ -425,12 +584,9 @@ async def setprefix(ctx, new_prefix: str):
await ctx . send ( " ❌ Prefix must be a single non-alphanumeric symbol (e.g. !, $, % , ?) " )
await ctx . send ( " ❌ Prefix must be a single non-alphanumeric symbol (e.g. !, $, % , ?) " )
return
return
# Update prefix
global PREFIX
PREFIX = new_prefix
PREFIX = new_prefix
bot . command_prefix = PREFIX
bot . command_prefix = PREFIX
# Write to .env
lines = [ ]
lines = [ ]
with open ( " .env " , " r " ) as f :
with open ( " .env " , " r " ) as f :
for line in f :
for line in f :
@@ -438,13 +594,14 @@ async def setprefix(ctx, new_prefix: str):
lines . append ( f " PREFIX= { new_prefix } \n " )
lines . append ( f " PREFIX= { new_prefix } \n " )
else :
else :
lines . append ( line )
lines . append ( line )
with open ( " .env " , " w " ) as f :
with open ( " .env " , " w " ) as f : f . writelines ( lines )
f . writelines ( lines )
await ctx . send ( f " ✅ Command prefix updated to ` { new_prefix } ` " )
await ctx . send ( f " ✅ Command prefix updated to ` { new_prefix } ` " )
@bot.command ( )
@bot.command ( )
async def updates ( ctx ) :
async def updates ( ctx ) :
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 ( member ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
@@ -454,20 +611,49 @@ async def updates(ctx):
response = requests . get ( VERSION_URL , timeout = 10 )
response = requests . get ( VERSION_URL , timeout = 10 )
if response . status_code == 200 :
if response . status_code == 200 :
latest_version = response . text . strip ( )
latest_version = response . text . strip ( )
await ctx . send (
await ctx . send ( f " 🤖 Bot version: ` { BOT_VERSION } ` \n 🌍 Latest version: ` { latest_version } ` \n { ' ✅ Up to date! ' if BOT_VERSION == latest_version else f ' ⚠️ Update available! Get it here: { RELEASES_URL } ' } " )
f " 🤖 Bot version: ` { BOT_VERSION } ` \n "
f " 🌍 Latest version: ` { latest_version } ` \n "
f " { ' ✅ Up to date! ' if BOT_VERSION == latest_version else f ' ⚠️ Update available! Get it here: { RELEASES_URL } ' } "
)
else :
else :
await ctx . send ( " ❌ Failed to fetch latest version info. " )
await ctx . send ( " ❌ Failed to fetch latest version info. " )
except Exception as e :
except Exception as e :
await ctx . send ( f " ❌ Error checking version: { e } " )
await ctx . send ( f " ❌ Error checking version: { e } " )
@bot.command ( )
async def logging ( ctx , state : str ) :
""" Admin-only: Enable or disable event logging. """
member = ctx . guild . get_member ( ctx . author . id )
if not member or not has_admin_role ( member ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
global EVENT_LOGGING
if state . lower ( ) in ( " on " , " true " , " 1 " ) :
EVENT_LOGGING = True
new_value = " true "
elif state . lower ( ) in ( " off " , " false " , " 0 " ) :
EVENT_LOGGING = False
new_value = " false "
else :
await ctx . send ( " ❌ Invalid value. Use `on` or `off`. " )
return
# Update .env
lines = [ ]
with open ( " .env " , " r " ) as f :
for line in f :
if line . startswith ( " EVENT_LOGGING= " ) :
lines . append ( f " EVENT_LOGGING= { new_value } \n " )
else :
lines . append ( line )
with open ( " .env " , " w " ) as f :
f . writelines ( lines )
await ctx . send ( f " ✅ Event logging is now { ' enabled ' if EVENT_LOGGING else ' disabled ' } . " )
log_event ( f " EVENT_LOGGING toggled to { new_value } by { ctx . author } " )
@bot.command ( name = " help " )
@bot.command ( name = " help " )
async def help_command ( ctx ) :
async def help_command ( ctx ) :
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 ( member )
@@ -496,6 +682,7 @@ async def help_command(ctx):
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 bots 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 "
) , inline = False )
) , inline = False )
await ctx . send ( embed = embed )
await ctx . send ( embed = embed )
@@ -522,6 +709,7 @@ async def daily_check():
# Log last run timestamp
# Log last run timestamp
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 } " )
@tasks.loop ( hours = 1 )
@tasks.loop ( hours = 1 )
async def check_for_updates ( ) :
async def check_for_updates ( ) :
@@ -533,11 +721,12 @@ async def check_for_updates():
log_channel = bot . get_channel ( SYNC_LOG_CHANNEL_ID )
log_channel = bot . get_channel ( SYNC_LOG_CHANNEL_ID )
if log_channel :
if log_channel :
await log_channel . send (
await log_channel . send (
f " ⚠️ **Update available for Jellyfin Bot!** \n "
f " 📌 Current version: ` { BOT_VERSION } ` \n "
f " 📌 Current version: ` { BOT_VERSION } ` \n "
f " ⬆️ Latest version: ` { latest_version } ` \n \n "
f " ⬆️ Latest version: ` { latest_version } ` \n "
f " 🔗 Download/update here: { RELEASES_URL } "
f " ⚠️ **Update available for Jellyfin Bot! Get it here:** \n \n "
f " { RELEASES_URL } "
)
)
log_event ( f " Latest Version: ' { latest_version } ' , Current Version: ' { BOT_VERSION } ' " )
except Exception as e :
except Exception as e :
print ( f " [Update Check] Failed: { e } " )
print ( f " [Update Check] Failed: { e } " )