@@ -8,6 +8,7 @@ from dotenv import load_dotenv
import pytz
import random
import qbittorrentapi
from proxmoxer import ProxmoxAPI
# =====================
# ENV + VALIDATION
@@ -40,6 +41,8 @@ JELLYSEERR_API_KEY = os.getenv("JELLYSEERR_API_KEY", "")
ENABLE_JFA = os . getenv ( " ENABLE_JFA " , " False " ) . lower ( ) == " true "
JFA_URL = os . getenv ( " JFA_URL " )
JFA_USERNAME = os . getenv ( " JFA_USERNAME " )
JFA_PASSWORD = os . getenv ( " JFA_PASSWORD " )
JFA_API_KEY = os . getenv ( " JFA_API_KEY " )
ENABLE_QBITTORRENT = os . getenv ( " ENABLE_QBITTORRENT " , " False " ) . lower ( ) == " true "
@@ -47,6 +50,15 @@ QBIT_HOST = os.getenv("QBIT_HOST")
QBIT_USERNAME = os . getenv ( " QBIT_USERNAME " )
QBIT_PASSWORD = os . getenv ( " QBIT_PASSWORD " )
ENABLE_PROXMOX = os . getenv ( " ENABLE_PROXMOX " , " False " ) . lower ( ) == " true "
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 " )
DB_PASSWORD = get_env_var ( " DB_PASSWORD " )
@@ -54,9 +66,10 @@ DB_NAME = get_env_var("DB_NAME")
LOCAL_TZ = pytz . timezone ( get_env_var ( " LOCAL_TZ " , str , required = False ) or " America/Chicago " )
BOT_VERSION = " 1.0.6 "
BOT_VERSION = " 1.0.7 "
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
@@ -81,15 +94,23 @@ bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None)
# QBITTORRENT SETUP
# =====================
qb = qbittorrentapi . Client (
if ENABLE_QBITTORRENT :
qb = qbittorrentapi . Client (
host = QBIT_HOST ,
username = QBIT_USERNAME ,
password = QBIT_PASSWORD
)
try :
)
try :
qb . auth_log_in ( )
except qbittorrentapi . LoginFailed :
print ( " Failed to log in to qBittorrent API " )
print ( " ✅ Logged in to qBittorrent " )
except qbittorrentapi . LoginFailed :
print ( " ❌ Failed to log in to qBittorrent API " )
qb = None
else :
qb = None # qBittorrent disabled
# =====================
# DATABASE SETUP
@@ -282,6 +303,32 @@ 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 )
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
# =====================
# JELLYSEERR HELPERS
# =====================
@@ -348,6 +395,109 @@ def progress_bar(progress: float, length: int = 20) -> str:
bar = ' █ ' * filled_length + ' ░ ' * ( length - filled_length )
return f " [ { bar } ] { progress * 100 : .2f } % "
# =====================
# PROXMOX HELPERS
# =====================
def get_proxmox_client ( ) :
""" Create and return a Proxmox client using API token auth. """
if not ( PROXMOX_HOST and PROXMOX_TOKEN_NAME and PROXMOX_TOKEN_VALUE ) :
raise ValueError ( " Proxmox API credentials are not fully configured in .env " )
# Parse host + port safely
base_host = PROXMOX_HOST . replace ( " https:// " , " " ) . replace ( " http:// " , " " )
if " : " in base_host :
host , port = base_host . split ( " : " )
else :
host , port = base_host , 8006 # default Proxmox port
# Split token into user + token name
try :
user , token_name = PROXMOX_TOKEN_NAME . split ( " ! " )
except ValueError :
raise ValueError (
" ❌ PROXMOX_TOKEN_NAME must be in the format ' user@realm!tokenid ' "
)
log_event ( f " [Proxmox] Connecting to { host } : { port } as { user } with token ' { token_name } ' " )
return ProxmoxAPI (
host ,
port = int ( port ) ,
user = user ,
token_name = token_name ,
token_value = PROXMOX_TOKEN_VALUE ,
verify_ssl = PROXMOX_VERIFY_SSL
)
# =====================
# JFA-GO HELPERS
# =====================
def refresh_jfa_token ( ) - > bool :
"""
Authenticate to JFA-Go with username/password (Basic auth) against /token/login,
write the returned token to .env (JFA_TOKEN and JFA_API_KEY), and reload env.
Returns True on success.
"""
global JFA_TOKEN , JFA_API_KEY
if not ( JFA_URL and JFA_USERNAME and JFA_PASSWORD ) :
print ( " [JFA] Missing JFA_URL/JFA_USERNAME/JFA_PASSWORD in environment. " )
return False
url = JFA_URL . rstrip ( " / " ) + " /token/login "
headers = { " accept " : " application/json " }
try :
# Option A: let requests build the Basic header
r = requests . get ( url , auth = ( JFA_USERNAME , JFA_PASSWORD ) , headers = headers , timeout = 10 )
# If you prefer to build the header manually (exactly like your curl), use:
# creds = f"{JFA_USERNAME}:{JFA_PASSWORD}".encode()
# b64 = base64.b64encode(creds).decode()
# headers["Authorization"] = f"Basic {b64}"
# r = requests.get(url, headers=headers, timeout=10)
if r . status_code != 200 :
print ( f " [JFA] token login failed: { r . status_code } - { r . text } " )
return False
data = r . json ( ) if r . text else { }
# try common token fields
token = (
data . get ( " token " )
or data . get ( " access_token " )
or data . get ( " jwt " )
or data . get ( " api_key " )
or data . get ( " data " ) # sometimes nested
)
# If API returns {"token": "<token>"} -> good. If it returns a wrapped structure,
# try to handle a couple of other shapes:
if not token :
# if response is {'success': True} or {'invites':...} then no token present
# print for debugging
print ( " [JFA] token not found in response JSON: " , data )
return False
# Persist token to .env under both names (compatibility)
_update_env_key ( " JFA_TOKEN " , token )
_update_env_key ( " JFA_API_KEY " , token )
# Update in-memory values and reload env
JFA_TOKEN = token
JFA_API_KEY = token
load_dotenv ( override = True )
print ( " [JFA] Successfully refreshed token and updated .env " )
return True
except Exception as e :
print ( f " [JFA] Exception while refreshing token: { e } " , exc_info = True )
return False
# =====================
# DISCORD HELPERS
# =====================
@@ -403,31 +553,25 @@ def get_metadata(key):
conn . close ( )
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 )
def _update_env_key ( key : str , value : str , env_path : str = " .env " ) :
""" Update or append key=value in .env (keeps file order). """
lines = [ ]
found = False
try :
with open ( env_path , " r " , encoding = " utf-8 " ) as f :
lines = f . readlines ( )
except FileNotFoundError :
lines = [ ]
if response . status_code == 200 :
return response . json ( ) . get ( " Id " )
with open ( env_path , " w " , encoding = " utf-8 " ) as f :
for line in lines :
if line . strip ( ) . startswith ( f " { key } = " ) :
f . write ( f " { key } = { value } \n " )
found = True
else :
print ( f " [Jellyfin] Trial user creation failed. Status: { response . status_code } , Response: { response . text } " )
return None
f . write ( line )
if not found :
f . write ( f " { key } = { value } \n " )
# =====================
@@ -752,6 +896,27 @@ async def clearinvites(ctx):
print ( f " [clearinvites] Error: { e } " , exc_info = True )
@bot.command ( )
async def refreshjfakey ( ctx ) :
""" Admin-only: Force refresh the JFA-Go API key using username/password auth. """
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
if not ENABLE_JFA :
await ctx . send ( " ⚠️ JFA-Go integration is disabled in the configuration. " )
return
await ctx . send ( " 🔁 Attempting to refresh JFA token... " )
success = refresh_jfa_token ( )
if success :
await ctx . send ( " ✅ Successfully refreshed the JFA-Go API token and updated `.env` " )
log_event ( f " Admin { ctx . author } forced a JFA API Token refresh " )
else :
await ctx . send ( " ❌ Failed to refresh the JFA-Go API token. Check bot logs for details. " )
log_event ( f " Admin { ctx . author } attempted JFA API Token refresh but failed " )
@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. """
@@ -888,8 +1053,9 @@ async def deleteaccount(ctx, username: str = None):
await ctx . send ( f " ❌ Failed to delete Jellyfin account ** { username } **. " )
@bot.command ( )
async def what 2watch( ctx ) :
""" Pick 5 random movies from the Jellyfin library with embeds and posters. """
async def movies 2watch( ctx ) :
log_event ( f " movies2watch invoked by { ctx . author } " )
""" Pick 5 random movies from the Jellyfin library with embeds, and IMDb links. """
member = ctx . guild . get_member ( ctx . author . id ) if ctx . guild else None
if not member or not has_required_role ( member ) :
await ctx . send ( f " ❌ { ctx . author . mention } , you don’ t have the required role to use this command. " )
@@ -897,8 +1063,17 @@ async def what2watch(ctx):
headers = { " X-Emby-Token " : JELLYFIN_API_KEY }
try :
# Fetch all movies
r = requests . get ( f " { JELLYFIN_URL } /Items?IncludeItemTypes=Movie&Recursive=true " , headers = headers , timeout = 10 )
# Fetch all movies (include ProviderIds explicitly!)
r = requests . get (
f " { JELLYFIN_URL } /Items " ,
headers = headers ,
params = {
" IncludeItemTypes " : " Movie " ,
" Recursive " : " true " ,
" Fields " : " ProviderIds "
} ,
timeout = 10
)
if r . status_code != 200 :
await ctx . send ( f " ❌ Failed to fetch movies. Status code: { r . status_code } " )
return
@@ -918,7 +1093,7 @@ async def what2watch(ctx):
)
for movie in selection :
name = movie . get ( " Name " )
name = movie . get ( " Name " , " Unknown Title " )
year = movie . get ( " ProductionYear " , " N/A " )
runtime = movie . get ( " RunTimeTicks " , None )
runtime_min = int ( runtime / 10_000_000 / 60 ) if runtime else " N/A "
@@ -928,11 +1103,16 @@ async def what2watch(ctx):
if " PrimaryImageTag " in movie and movie [ " PrimaryImageTag " ] :
poster_url = f " { JELLYFIN_URL } /Items/ { movie [ ' Id ' ] } /Images/Primary?tag= { movie [ ' PrimaryImageTag ' ] } &quality=90 "
field_value = f " Year: { year } \n Runtime: { runtime_min } min "
# IMDb link if available
imdb_id = movie . get ( " ProviderIds " , { } ) . get ( " Imdb " )
imdb_link = f " [IMDb Link](https://www.imdb.com/title/ { imdb_id } ) " if imdb_id else " No IMDb ID available "
# Field content
field_value = f " Year: { year } \n Runtime: { runtime_min } min \n { imdb_link } "
embed . add_field ( name = name , value = field_value , inline = False )
if poster_url :
embed . set_image ( url = poster_url ) # Only last movie's poster will appear as main embed image
embed . set_image ( url = poster_url ) # Only the last poster appears as main embed image
await ctx . send ( embed = embed )
@@ -940,6 +1120,72 @@ async def what2watch(ctx):
await ctx . send ( f " ❌ Error fetching movies: { e } " )
print ( f " [what2watch] Error: { e } " )
@bot.command ( )
async def shows2watch ( ctx ) :
log_event ( f " shows2watch invoked by { ctx . author } " )
""" Pick 5 random TV shows from the Jellyfin library with embeds, and IMDb links. """
member = ctx . guild . get_member ( ctx . author . id ) if ctx . guild else None
if not member or not has_required_role ( member ) :
await ctx . send ( f " ❌ { ctx . author . mention } , you don’ t have the required role to use this command. " )
return
headers = { " X-Emby-Token " : JELLYFIN_API_KEY }
try :
# Fetch all shows (include ProviderIds explicitly!)
r = requests . get (
f " { JELLYFIN_URL } /Items " ,
headers = headers ,
params = {
" IncludeItemTypes " : " Series " ,
" Recursive " : " true " ,
" Fields " : " ProviderIds "
} ,
timeout = 10
)
if r . status_code != 200 :
await ctx . send ( f " ❌ Failed to fetch shows. Status code: { r . status_code } " )
return
shows = r . json ( ) . get ( " Items " , [ ] )
if not shows :
await ctx . send ( " ⚠️ No shows found in the library. " )
return
# Pick 5 random shows
selection = random . sample ( shows , min ( 5 , len ( shows ) ) )
embed = discord . Embed (
title = " 📺 Shows to Watch " ,
description = " Here are 5 random TV show suggestions from the library: " ,
color = discord . Color . green ( )
)
for show in selection :
name = show . get ( " Name " , " Unknown Title " )
year = show . get ( " ProductionYear " , " N/A " )
# Poster URL if available
poster_url = None
if " PrimaryImageTag " in show and show [ " PrimaryImageTag " ] :
poster_url = f " { JELLYFIN_URL } /Items/ { show [ ' Id ' ] } /Images/Primary?tag= { show [ ' PrimaryImageTag ' ] } &quality=90 "
# IMDb link if available
imdb_id = show . get ( " ProviderIds " , { } ) . get ( " Imdb " )
imdb_link = f " [IMDb Link](https://www.imdb.com/title/ { imdb_id } ) " if imdb_id else " No IMDb ID available "
# Field content
field_value = f " Year: { year } \n { imdb_link } "
embed . add_field ( name = name , value = field_value , inline = False )
if poster_url :
embed . set_image ( url = poster_url ) # Only the last show's poster will appear as the embed image
await ctx . send ( embed = embed )
except Exception as e :
await ctx . send ( f " ❌ Error fetching shows: { e } " )
print ( f " [shows2watch] Error: { e } " )
@bot.command ( )
async def cleanup ( ctx ) :
@@ -1225,6 +1471,130 @@ 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. """
if not ENABLE_PROXMOX :
await ctx . send ( " ⚠️ Proxmox integration is disabled in the configuration. " )
return
if not has_admin_role ( ctx . author ) :
await ctx . send ( " ❌ You don’ t have permission to use this command. " )
return
try :
proxmox = get_proxmox_client ( )
embed = discord . Embed (
title = " 📦 Proxmox Storage " ,
description = " Storage pool usage and ZFS pools " ,
color = discord . Color . green ( )
)
for node in proxmox . nodes . get ( ) :
node_name = node [ " node " ]
# ---- ZFS ----
try :
zfs_pools = proxmox . nodes ( node_name ) . disks . zfs . get ( )
if zfs_pools :
zfs_info = [
f " ** { p [ ' name ' ] } **: { p [ ' alloc ' ] / 1024 * * 3 : .2f } GiB / "
f " { p [ ' size ' ] / 1024 * * 3 : .2f } GiB ( { ( p [ ' alloc ' ] / p [ ' size ' ] * 100 ) : .1f } %) "
for p in zfs_pools
]
else :
zfs_info = [ " No ZFS pools found " ]
except Exception as e :
zfs_info = [ f " ⚠️ Failed to fetch ZFS pools ( { e } ) " ]
embed . add_field (
name = f " 🖥️ { node_name } - ZFS Pools " ,
value = " \n " . join ( zfs_info ) ,
inline = False
)
# ---- Normal storage (skip ZFS) ----
try :
storage_info = proxmox . nodes ( node_name ) . storage . get ( )
normal_lines = [
f " ** { s [ ' storage ' ] } **: { s [ ' used ' ] / 1024 * * 3 : .2f } GiB / "
f " { s [ ' total ' ] / 1024 * * 3 : .2f } GiB ( { ( s [ ' used ' ] / s [ ' total ' ] * 100 ) : .1f } %) "
for s in storage_info
if s . get ( " type " , " " ) . lower ( ) not in ( " zfspool " , " zfs " )
]
except Exception as e :
normal_lines = [ f " ⚠️ Failed to fetch normal storage ( { e } ) " ]
embed . add_field (
name = f " 🗳️ { node_name } - Normal Storage " ,
value = " \n " . join ( normal_lines ) or " No non‑ ZFS storage found " ,
inline = False
)
await ctx . send ( embed = embed )
except Exception as e :
await ctx . send ( f " ⚠️ Unexpected error: { e } " )
@bot.command ( )
async def link ( ctx , jellyfin_username : str = None , user : discord . User = None , js_id : str = None ) :
log_event ( f " link invoked by { ctx . author } " )
@@ -1314,6 +1684,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. """
@@ -1365,7 +1787,8 @@ async def help_command(ctx):
f " ` { PREFIX } createaccount <username> <password>` - Create your Jellyfin account " ,
f " ` { PREFIX } recoveraccount <newpassword>` - Reset your password " ,
f " ` { PREFIX } deleteaccount <username>` - Delete your Jellyfin account " ,
f " ` { PREFIX } what 2watch` - Lists 5 random movie suggestions from the Jellyfin Library"
f " ` { PREFIX } movies 2watch` - Lists 5 random movie suggestions from the Jellyfin Library" ,
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 " )
@@ -1405,12 +1828,21 @@ async def help_command(ctx):
]
embed . add_field ( name = " 💾 qBittorrent Commands " , value = " \n " . join ( qb_cmds ) , inline = False )
# --- Proxmox Commands ---
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 )
# --- JFA Commands ---
if ENABLE_JFA :
jfa_cmds = [
f " ` { PREFIX } createinvite` - Create a new JFA invite link " ,
f " ` { PREFIX } listinvites` - List all active JFA invite links " ,
f " ` { PREFIX } deleteinvite <code>` - Delete a specific JFA invite "
f " ` { PREFIX } deleteinvite <code>` - Delete a specific JFA invite " ,
f " ` { PREFIX } refreshjfakey` - Refreshes the JFA API Key Forcefully "
]
embed . add_field ( name = " 🔑 JFA Commands " , value = " \n " . join ( jfa_cmds ) , inline = False )
@@ -1418,6 +1850,7 @@ async def help_command(ctx):
admin_bot_cmds = [
f " ` { PREFIX } setprefix` - Change the bot ' s command prefix " ,
f " ` { PREFIX } updates` - 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 )
@@ -1562,6 +1995,30 @@ async def cleanup_task():
except Exception as e :
print ( f " [Cleanup] Failed to send removed message to sync channel: { e } " )
# =====================
# JFA-Go Scheduled Token Refresh
# =====================
if ENABLE_JFA :
@tasks.loop ( hours = 1 )
async def refresh_jfa_loop ( ) :
success = refresh_jfa_token ( )
if success :
log_event ( " [JFA] Successfully refreshed token (scheduled loop). " )
else :
log_event ( " [JFA] Failed to refresh token (scheduled loop). " )
@refresh_jfa_loop.before_loop
async def before_refresh_jfa_loop ( ) :
await bot . wait_until_ready ( )
log_event ( " [JFA] Token refresh loop waiting until bot is ready. " )
# Start the loop inside on_ready to ensure event loop exists
@bot.event
async def on_ready ( ) :
if not refresh_jfa_loop . is_running ( ) :
refresh_jfa_loop . start ( )
log_event ( f " Bot is ready. Logged in as { bot . user } " )
@tasks.loop ( hours = 1 )
async def check_for_updates ( ) :
@@ -1575,7 +2032,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 } ' " )
@@ -1607,6 +2064,10 @@ async def on_ready():
if not cleanup_task . is_running ( ) :
cleanup_task . 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 ( )