@@ -9,6 +9,15 @@ import pytz
import random
import qbittorrentapi
from proxmoxer import ProxmoxAPI
import subprocess
import sys
import zipfile
import io
import time
from pathlib import Path
import tempfile
import shutil
import pymysql
# =====================
# ENV + VALIDATION
@@ -34,6 +43,7 @@ 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 " )
ENABLE_TRIAL_ACCOUNTS = os . getenv ( " ENABLE_TRIAL_ACCOUNTS " , " False " ) . lower ( ) == " true "
TRIAL_TIME = int ( os . getenv ( " TRIAL_TIME " , 24 ) )
JELLYSEERR_ENABLED = os . getenv ( " JELLYSEERR_ENABLED " , " false " ) . lower ( ) == " true "
JELLYSEERR_URL = os . getenv ( " JELLYSEERR_URL " , " " ) . rstrip ( " / " )
@@ -55,6 +65,9 @@ PROXMOX_HOST = os.getenv("PROXMOX_HOST")
PROXMOX_TOKEN_NAME = os . getenv ( " PROXMOX_TOKEN_NAME " )
PROXMOX_TOKEN_VALUE = os . getenv ( " PROXMOX_TOKEN_VALUE " )
PROXMOX_VERIFY_SSL = os . getenv ( " PROXMOX_VERIFY_SSL " , " False " ) . lower ( ) == " true "
PROXMOX_NODE = os . getenv ( " PROXMOX_NODE " , " pve " )
PROXMOX_VM_ID = os . getenv ( " PROXMOX_VM_ID " , None )
PROXMOX_TYPE = os . getenv ( " PROXMOX_TYPE " , " qemu " )
DB_HOST = get_env_var ( " DB_HOST " )
DB_USER = get_env_var ( " DB_USER " )
@@ -62,10 +75,14 @@ DB_PASSWORD = get_env_var("DB_PASSWORD")
DB_NAME = get_env_var ( " DB_NAME " )
LOCAL_TZ = pytz . timezone ( get_env_var ( " LOCAL_TZ " , str , required = False ) or " America/Chicago " )
ENV_FILE = " .env "
DEFAULT_ENV_FILE = " .env.example "
BACKUP_DIR = Path ( " backups " )
BOT_VERSION = " 1.0.7 "
BOT_VERSION = " 1.0.9 "
VERSION_URL = " https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt "
RELEASES_URL = " https://github.com/PenguCCN/Jellycord/releases "
CHANGELOG_URL = " https://raw.githubusercontent.com/PenguCCN/Jellycord/refs/heads/main/CHANGELOG.md "
# =====================
# EVENT LOGGING
@@ -569,6 +586,91 @@ def _update_env_key(key: str, value: str, env_path: str = ".env"):
if not found :
f . write ( f " { key } = { value } \n " )
def export_mysql_db ( dump_file ) :
try :
conn = mysql . connector . connect (
host = DB_HOST ,
user = DB_USER ,
password = DB_PASSWORD ,
database = DB_NAME
)
cursor = conn . cursor ( )
with open ( dump_file , " w " , encoding = " utf-8 " ) as f :
# Get tables
cursor . execute ( " SHOW TABLES " )
tables = [ row [ 0 ] for row in cursor . fetchall ( ) ]
for table in tables :
# Dump CREATE statement
cursor . execute ( f " SHOW CREATE TABLE ` { table } ` " )
create_stmt = cursor . fetchone ( ) [ 1 ]
f . write ( f " -- Table structure for ` { table } ` \n { create_stmt } ; \n \n " )
# Dump rows
cursor . execute ( f " SELECT * FROM ` { table } ` " )
rows = cursor . fetchall ( )
if rows :
columns = [ desc [ 0 ] for desc in cursor . description ]
for row in rows :
values = " , " . join (
f " ' { str ( val ) . replace ( " ' " , " ' ' " ) } ' " if val is not None else " NULL "
for val in row
)
f . write ( f " INSERT INTO ` { table } ` ( { ' , ' . join ( columns ) } ) VALUES ( { values } ); \n " )
f . write ( " \n " )
cursor . close ( )
conn . close ( )
return True
except Exception as e :
print ( f " [Backup] Database export failed: { e } " )
return False
def sync_env_file ( ) :
""" Ensure .env has all fields from .env.example, preserving existing values. """
if not os . path . exists ( DEFAULT_ENV_FILE ) :
print ( " [updatebot] No .env.example found, skipping env sync " )
return
# Load .env.example as baseline
with open ( DEFAULT_ENV_FILE , " r " ) as f :
default_lines = [ line . strip ( " \n " ) for line in f . readlines ( ) ]
# Load existing .env (create if missing)
existing = { }
if os . path . exists ( ENV_FILE ) :
with open ( ENV_FILE , " r " ) as f :
for line in f :
if " = " in line and not line . strip ( ) . startswith ( " # " ) :
key , val = line . split ( " = " , 1 )
existing [ key . strip ( ) ] = val . strip ( )
# Build new env content
new_lines = [ ]
for line in default_lines :
if " = " not in line : # comments or blank lines
new_lines . append ( line )
continue
key , default_val = line . split ( " = " , 1 )
key = key . strip ( )
if key in existing :
new_lines . append ( f " { key } = { existing [ key ] } " )
else :
new_lines . append ( line ) # use default if missing
# Write back updated .env
with open ( ENV_FILE , " w " ) as f :
f . write ( " \n " . join ( new_lines ) + " \n " )
print ( " [updatebot] Synced .env file successfully " )
def restart_bot ( ) :
""" Replace current process with a new one. """
os . execv ( sys . executable , [ sys . executable ] + sys . argv )
# =====================
# EVENTS
@@ -915,7 +1017,7 @@ async def refreshjfakey(ctx):
@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. """
f """ Create a { TRIAL_TIME } -hour trial Jellyfin account. DM-only, one-time per user. """
log_event ( f " trialaccount invoked by { ctx . author } " )
# Ensure trial accounts are enabled
@@ -983,7 +1085,7 @@ async def trialaccount(ctx, username: str = None, password: str = None):
cur . close ( )
conn . close ( )
await ctx . send ( f " ✅ Trial Jellyfin account ** { username } ** created! It will expire in 24 hours. \n 🌐 Login here: { JELLYFIN_URL } " )
await ctx . send ( f " ✅ Trial Jellyfin account ** { username } ** created! It will expire in { TRIAL_TIME } hours. \n 🌐 Login here: { JELLYFIN_URL } " )
log_event ( f " Trial account created for { ctx . author } ( { username } ) " )
else :
cur . close ( )
@@ -1219,7 +1321,7 @@ async def cleanup(ctx):
await ctx . send ( " ✅ Cleanup complete. " )
@bot.command ( )
async def list validusers( ctx ) :
async def validusers ( ctx ) :
""" Admin-only: List how many registered users have a valid role. """
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
@@ -1467,6 +1569,65 @@ async def qbview(ctx):
await ctx . send ( embed = embed )
@bot.command ( )
async def metrics ( ctx ) :
""" Check performance metrics for the configured Proxmox VM/Container. """
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
if not PROXMOX_VM_ID :
await ctx . send ( " ⚠️ No Proxmox VM/Container ID is set in the .env file. " )
return
headers = {
" Authorization " : f " PVEAPIToken= { PROXMOX_TOKEN_NAME } = { PROXMOX_TOKEN_VALUE } "
}
try :
url = f " { PROXMOX_HOST } /api2/json/nodes/ { PROXMOX_NODE } / { PROXMOX_TYPE } / { PROXMOX_VM_ID } /status/current "
r = requests . get ( url , headers = headers , verify = False , timeout = 10 )
if r . status_code != 200 :
await ctx . send ( f " ❌ Failed to fetch VM/Container status (status { r . status_code } ) " )
return
data = r . json ( ) . get ( " data " , { } )
# Extract metrics
name = data . get ( " name " , f " ID { PROXMOX_VM_ID } " )
status = data . get ( " status " , " unknown " ) . capitalize ( )
cpu = round ( data . get ( " cpu " , 0 ) * 100 , 2 ) # returns fraction, convert to %
maxmem = data . get ( " maxmem " , 1 )
mem = data . get ( " mem " , 0 )
mem_usage = round ( ( mem / maxmem ) * 100 , 2 ) if maxmem > 0 else 0
maxdisk = data . get ( " maxdisk " , 1 )
disk = data . get ( " disk " , 0 )
disk_usage = round ( ( disk / maxdisk ) * 100 , 2 ) if maxdisk > 0 else 0
maxswap = data . get ( " maxswap " , 1 )
swap = data . get ( " swap " , 0 )
swap_usage = round ( ( swap / maxswap ) * 100 , 2 ) if maxswap > 0 else 0
uptime = data . get ( " uptime " , 0 )
# Build embed
embed = discord . Embed (
title = f " 📊 Proxmox Status: { name } " ,
color = discord . Color . green ( ) if status == " Running " else discord . Color . red ( )
)
embed . add_field ( name = " Status " , value = status , inline = True )
embed . add_field ( name = " CPU Usage " , value = f " { cpu } % " , inline = True )
embed . add_field ( name = " Memory Usage " , value = f " { mem_usage } % " , inline = True )
embed . add_field ( name = " Disk Usage " , value = f " { disk_usage } % " , inline = True )
embed . add_field ( name = " Swap Usage " , value = f " { swap_usage } % " , inline = True )
embed . add_field ( name = " Uptime " , value = f " { uptime / / 3600 } h { ( uptime % 3600 ) / / 60 } m " , inline = True )
await ctx . send ( embed = embed )
except Exception as e :
await ctx . send ( f " ❌ Error fetching Proxmox VM/Container status: { e } " )
print ( f " [proxmoxstatus] Error: { e } " )
@bot.command ( )
async def storage ( ctx ) :
""" Check Proxmox storage pools and ZFS pools. """
@@ -1533,9 +1694,9 @@ async def storage(ctx):
@bot.command ( )
async def link ( ctx , jellyfin_username : str = None , user: discord . User = None , js_id : str = None ) :
async def link ( ctx , user : discord . User = None , jellyfin_username : st r = None , js_id : str = None ) :
log_event ( f " link invoked by { ctx . author } " )
usage_args = [ " <Jellyfin Account> " , " <@user > " ]
usage_args = [ " <@user> " , " <Jellyfin Account > " ]
if JELLYSEERR_ENABLED : usage_args . append ( " <Jellyseerr ID> " )
if jellyfin_username is None or user is None or ( JELLYSEERR_ENABLED and js_id is None ) :
@@ -1553,7 +1714,7 @@ async def link(ctx, jellyfin_username: str = None, user: discord.User = None, js
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 { user . mention } to Jellyfin account ** { jellyfin_username } **. " )
@bot.command ( )
@@ -1604,7 +1765,247 @@ async def setprefix(ctx, new_prefix: str = None):
@bot.command ( )
async def updates ( ctx ) :
async def update ( ctx ) :
""" Admin-only: Check GitHub version, sync .env, and pull latest bot code. """
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
try :
# Fetch latest version
version_url = " https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt "
r = requests . get ( version_url , timeout = 10 )
if r . status_code != 200 :
await ctx . send ( " ❌ Failed to fetch latest version info. " )
return
latest_version = r . text . strip ( )
if latest_version == BOT_VERSION :
await ctx . send ( f " ✅ Bot is already up-to-date (` { BOT_VERSION } `). " )
return
await ctx . send ( f " ⬆️ Update found: ` { BOT_VERSION } ` → ` { latest_version } ` " )
# Download release zip
releases_url = " https://github.com/PenguCCN/Jellycord/releases/latest/download/Jellycord.zip "
r = requests . get ( releases_url , timeout = 30 )
if r . status_code != 200 :
await ctx . send ( " ❌ Failed to download latest release zip. " )
return
with zipfile . ZipFile ( io . BytesIO ( r . content ) ) as z :
z . extractall ( " update_tmp " )
# Merge .env with .env.example
env_path = " .env "
example_path = os . path . join ( " update_tmp " , " .env.example " )
if os . path . exists ( example_path ) :
# Load current env into dict
current_env = { }
if os . path . exists ( env_path ) :
with open ( env_path , " r " ) as f :
for line in f :
if " = " in line and not line . strip ( ) . startswith ( " # " ) :
key , val = line . split ( " = " , 1 )
current_env [ key . strip ( ) ] = val . strip ( )
merged_lines = [ ]
with open ( example_path , " r " ) as f :
for line in f :
if line . strip ( ) . startswith ( " # " ) or " = " not in line :
# Keep comments & blank lines exactly as they are
merged_lines . append ( line . rstrip ( " \n " ) )
else :
key , default_val = line . split ( " = " , 1 )
key = key . strip ( )
if key in current_env :
merged_lines . append ( f " { key } = { current_env [ key ] } " )
else :
merged_lines . append ( line . rstrip ( " \n " ) )
with open ( env_path , " w " ) as f :
f . write ( " \n " . join ( merged_lines ) + " \n " )
# Overwrite all other bot files
for root , dirs , files in os . walk ( " update_tmp " ) :
for file in files :
if file == " .env.example " :
continue
src = os . path . join ( root , file )
dst = os . path . relpath ( src , " update_tmp " )
os . replace ( src , dst )
await ctx . send ( f " ✅ Update applied! Now running version ` { latest_version } `. \n ⚠️ Restart the bot to load changes. " )
restart_bot ( )
except Exception as e :
await ctx . send ( f " ❌ Update failed: { e } " )
print ( f " [updatebot] Error: { e } " )
@bot.command ( )
async def backup ( ctx ) :
""" Create a backup of the bot (files + DB). """
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
await ctx . send ( " 📦 Starting backup process... " )
try :
BACKUP_DIR . mkdir ( exist_ok = True )
# Backup filename
today = datetime . datetime . now ( ) . strftime ( " % m- %d - % Y " )
backup_name = f " { today } - { BOT_VERSION } .zip "
backup_path = BACKUP_DIR / backup_name
# Temporary SQL dump file
dump_file = BACKUP_DIR / f " { DB_NAME } .sql "
if not export_mysql_db ( dump_file ) :
await ctx . send ( " ⚠️ Database export failed, continuing without DB dump... " )
with zipfile . ZipFile ( backup_path , " w " , zipfile . ZIP_DEFLATED ) as backup_zip :
# Add all files in current directory (skip backups themselves)
for root , _ , files in os . walk ( " . " ) :
if root . startswith ( " ./backups " ) :
continue
for file in files :
file_path = Path ( root ) / file
backup_zip . write ( file_path , arcname = file_path . relative_to ( " . " ) )
# Add DB dump if created
if dump_file . exists ( ) :
backup_zip . write ( dump_file , arcname = f " { DB_NAME } .sql " )
dump_file . unlink ( ) # remove temporary dump file
await ctx . send ( f " ✅ Backup created: ` { backup_name } ` " )
log_event ( f " Backup created: { backup_name } " )
except Exception as e :
await ctx . send ( f " ❌ Backup failed: { e } " )
print ( f " [Backup] Error: { e } " )
@bot.command ( )
async def restore ( ctx , backup_file : str ) :
""" Restore a backup (files + database) from a zip. Admin only. """
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
backup_path = os . path . join ( " backups " , backup_file )
if not os . path . exists ( backup_path ) :
await ctx . send ( f " ❌ Backup ` { backup_file } ` not found. " )
return
await ctx . send ( f " ♻️ Starting restore from ` { backup_file } `. This may take a while... " )
temp_dir = os . path . join ( " backups " , " restore_temp " )
os . makedirs ( temp_dir , exist_ok = True )
try :
# --- Extract zip to local restore_temp folder ---
with zipfile . ZipFile ( backup_path , " r " ) as zip_ref :
zip_ref . extractall ( temp_dir )
# --- Database Restore ---
sql_files = [ f for f in os . listdir ( temp_dir ) if f . endswith ( " .sql " ) ]
if sql_files :
sql_file_path = os . path . join ( temp_dir , sql_files [ 0 ] )
with open ( sql_file_path , " r " , encoding = " utf-8 " ) as f :
sql_content = f . read ( )
conn = pymysql . connect (
host = os . getenv ( " DB_HOST " , " localhost " ) ,
user = os . getenv ( " DB_USER " ) ,
password = os . getenv ( " DB_PASSWORD " ) ,
database = os . getenv ( " DB_NAME " ) ,
autocommit = True
)
with conn . cursor ( ) as cursor :
cursor . execute ( " SET FOREIGN_KEY_CHECKS = 0; " )
cursor . execute ( " SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE(); " )
tables = cursor . fetchall ( )
for ( table_name , ) in tables :
cursor . execute ( f " DROP TABLE IF EXISTS ` { table_name } `; " )
cursor . execute ( " SET FOREIGN_KEY_CHECKS = 1; " )
for statement in sql_content . split ( " ; " ) :
stmt = statement . strip ( )
if stmt :
cursor . execute ( stmt )
conn . close ( )
await ctx . send ( " ✅ Database restored successfully! " )
else :
await ctx . send ( " ⚠️ No SQL backup found in this zip file. " )
# --- Copy files to working directory ---
for item in os . listdir ( temp_dir ) :
src_path = os . path . join ( temp_dir , item )
dest_path = os . path . join ( " . " , item )
if os . path . isdir ( src_path ) :
if os . path . exists ( dest_path ) :
shutil . rmtree ( dest_path )
shutil . copytree ( src_path , dest_path )
else :
shutil . copy2 ( src_path , dest_path )
await ctx . send ( " ✅ Files restored successfully! " )
except Exception as e :
await ctx . send ( f " ❌ Restore failed: { e } " )
return
finally :
# --- Clean up restore_temp folder ---
shutil . rmtree ( temp_dir , ignore_errors = True )
await ctx . send ( " 🔃 Restarting bot to apply changes... " )
restart_bot ( )
@bot.command ( )
async def backups ( ctx ) :
""" List all available backups in the backups directory (newest to oldest). """
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
backup_folder = Path ( " backups " )
if not backup_folder . exists ( ) :
await ctx . send ( " ⚠️ No backups folder found. " )
return
# Collect all zip files in backups dir
backups = list ( backup_folder . glob ( " *.zip " ) )
if not backups :
await ctx . send ( " ⚠️ No backups found. " )
return
# Sort by modification time, newest first
backups . sort ( key = lambda f : f . stat ( ) . st_mtime , reverse = True )
embed = discord . Embed (
title = " 📂 Available Backups " ,
description = " Newest to oldest backups: " ,
color = discord . Color . green ( )
)
for backup in backups :
mtime = backup . stat ( ) . st_mtime
formatted_time = f " <t: { int ( mtime ) } :f> " # Discord timestamp formatting
embed . add_field (
name = backup . name ,
value = f " Created: { formatted_time } " ,
inline = False
)
await ctx . send ( embed = embed )
@bot.command ( )
async def version ( ctx ) :
log_event ( f " updates invoked by { ctx . author } " )
member = ctx . guild . get_member ( ctx . author . id )
if not has_admin_role ( ctx . author ) :
@@ -1621,6 +2022,58 @@ async def updates(ctx):
except Exception as e :
await ctx . send ( f " ❌ Error checking version: { e } " )
@bot.command ( )
async def changelog ( ctx ) :
log_event ( f " changelog invoked by { ctx . author } " )
""" Fetch and display the changelog for the current bot version. """
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
try :
r = requests . get ( CHANGELOG_URL , timeout = 10 )
if r . status_code != 200 :
await ctx . send ( f " ❌ Failed to fetch changelog (status { r . status_code } ) " )
return
changelog_text = r . text
# Find the section for the current version
search_str = f " # { BOT_VERSION } "
start_idx = changelog_text . find ( search_str )
if start_idx == - 1 :
await ctx . send ( f " ⚠️ No changelog found for version ` { BOT_VERSION } `. " )
return
# Find the next heading or end of file
next_idx = changelog_text . find ( " # " , start_idx + len ( search_str ) )
if next_idx == - 1 :
section = changelog_text [ start_idx : ] . strip ( )
else :
section = changelog_text [ start_idx : next_idx ] . strip ( )
# Clean the section (remove the "# version" line itself)
lines = section . splitlines ( )
if lines and lines [ 0 ] . startswith ( " # " ) :
lines = lines [ 1 : ]
section_content = " \n " . join ( lines ) . strip ( )
if not section_content :
section_content = " ⚠️ No details provided for this version. "
embed = discord . Embed (
title = f " 📜 Changelog for v { BOT_VERSION } " ,
description = section_content ,
color = discord . Color . purple ( )
)
await ctx . send ( embed = embed )
except Exception as e :
await ctx . send ( f " ❌ Error fetching changelog: { e } " )
print ( f " [changelog] Error: { e } " )
@bot.command ( )
async def logging ( ctx , state : str ) :
""" Admin-only: Enable or disable event logging. """
@@ -1676,7 +2129,7 @@ async def help_command(ctx):
f " ` { PREFIX } shows2watch` - Lists 5 random show suggestions from the Jellyfin Library "
]
if ENABLE_TRIAL_ACCOUNTS :
user_cmds . append ( f " ` { PREFIX } trialaccount <username> <password>` - Create a 24 -hour trial Jellyfin account " )
user_cmds . append ( f " ` { PREFIX } trialaccount <username> <password>` - Create a { TRIAL_TIME } -hour trial Jellyfin account " )
embed . add_field ( name = " 🎬 Jellyfin Commands " , value = " \n " . join ( user_cmds ) , inline = False )
@@ -1689,14 +2142,14 @@ async def help_command(ctx):
# --- Admin Commands ---
if is_admin :
# Admin Jellyfin commands
link_command = f " ` { PREFIX } link <jellyfin_username> @user ` - Manually link accounts "
link_command = f " ` { PREFIX } link @user <jellyfin_username>` - Manually link accounts "
if JELLYSEERR_ENABLED :
link_command = f " ` { PREFIX } link <jellyfin_username> @user <Jellyseerr ID>` - Link accounts with Jellyseerr "
link_command = f " ` { PREFIX } link @user <jellyfin_username> <Jellyseerr ID>` - Link accounts with Jellyseerr "
admin_cmds = [
link_command ,
f " ` { PREFIX } unlink @user` - Manually unlink accounts " ,
f " ` { PREFIX } list validusers` - Show number of valid and invalid accounts" ,
f " ` { PREFIX } validusers` - Show number of valid and invalid accounts " ,
f " ` { PREFIX } cleanup` - Remove Jellyfin accounts from users without roles " ,
f " ` { PREFIX } lastcleanup` - See last cleanup time and time remaining " ,
f " ` { PREFIX } searchaccount <jellyfin_username>` - Find linked Discord user " ,
@@ -1717,6 +2170,7 @@ async def help_command(ctx):
if ENABLE_PROXMOX :
qb_cmds = [
f " ` { PREFIX } storage` - Show available storage pools and free space " ,
f " ` { PREFIX } metrics` - Show Jellyfin container metrics "
]
embed . add_field ( name = " 🗳️ Proxmox Commands " , value = " \n " . join ( qb_cmds ) , inline = False )
@@ -1733,7 +2187,12 @@ async def help_command(ctx):
# Admin Bot commands
admin_bot_cmds = [
f " ` { PREFIX } setprefix` - Change the bot ' s command prefix " ,
f " ` { PREFIX } updates ` - Manually check for bot updates " ,
f " ` { PREFIX } update` - Download latest bot version " ,
f " ` { PREFIX } backup` - Create a backup of the bot, its database and configurations " ,
f " ` { PREFIX } backups` - List backups of the bot " ,
f " ` { PREFIX } restore` - Restore a backup of the bot " ,
f " ` { PREFIX } version` - Manually check for bot updates " ,
f " ` { PREFIX } changelog` - View changelog for current bot version " ,
f " ` { PREFIX } logging` - Enable/disable console event logging "
]
embed . add_field ( name = " ⚙️ Admin Bot Commands " , value = " \n " . join ( admin_bot_cmds ) , inline = False )
@@ -1821,7 +2280,7 @@ async def cleanup_task():
else :
created_at_local = created_at_utc . astimezone ( LOCAL_TZ )
if now_local > created_at_local + datetime . timedelta ( hours = 24 ) :
if now_local > created_at_local + datetime . timedelta ( hours = TRIAL_TIME ) :
# Delete trial Jellyfin user
try :
delete_jellyfin_user ( trial . get ( " jellyfin_username " ) )
@@ -1915,7 +2374,7 @@ async def check_for_updates():
await log_channel . send (
f " 📌 Current version: ` { BOT_VERSION } ` \n "
f " ⬆️ Latest version: ` { latest_version } ` \n "
f " ⚠️ **Update available for Jellyfin Bot ! Get it here:** \n \n "
f " ⚠️ **Update available for Jellycord ! Get it here:** \n \n "
f " { RELEASES_URL } "
)
log_event ( f " Latest Version: ' { latest_version } ' , Current Version: ' { BOT_VERSION } ' " )
@@ -1947,8 +2406,9 @@ async def on_ready():
if not cleanup_task . is_running ( ) :
cleanup_task . start ( )
if not refresh_jfa_loop . is_running ( ) :
refresh_jfa_loop . start ( )
if ENABLE_JFA :
if not refresh_jfa_loop . is_running ( ) :
refresh_jfa_loop . start ( )
if not check_for_updates . is_running ( ) :
check_for_updates . start ( )