9 Commits
1.0.7 ... 1.0.9

Author SHA1 Message Date
42b10a813f Added Restoring, Backups, and Updating 2025-09-26 21:46:18 -05:00
bae9a5b967 Reformatted Link Command 2025-09-25 16:21:21 -05:00
b1d050cb3c Configurable Trial Duration 2025-09-25 16:01:10 -05:00
ce076824a2 Update app.py 2025-09-21 23:00:29 -05:00
8fe3d2e5d1 Update version.txt 2025-09-21 22:58:56 -05:00
e0bbbd02f5 1.0.8 2025-09-21 22:58:39 -05:00
d80746cd61 Added container metrics tracking 2025-09-21 22:48:01 -05:00
f6197c144a Fixed Schedule Loop for JFA
- Added Changelog Command
- Fixed schedule loop for Jfa being enabled when JFA support is disabled
2025-09-21 20:43:34 -05:00
2481ba778b Fixed update message 2025-09-21 18:19:53 -05:00
7 changed files with 519 additions and 24 deletions

View File

@@ -1,7 +1,9 @@
#Jellycord version 1.0.9
# Discord
DISCORD_TOKEN=your_discord_bot_token
PREFIX=!
GUILD_ID=123456789012345678,123456789012345678
GUILD_IDS=123456789012345678,123456789012345678
ADMIN_ROLE_IDS=111111111111111111,222222222222222222
REQUIRED_ROLE_IDS=333333333333333333,444444444444444444
@@ -9,6 +11,7 @@ REQUIRED_ROLE_IDS=333333333333333333,444444444444444444
JELLYFIN_URL=http://127.0.0.1:8096
JELLYFIN_API_KEY=your_jellyfin_api_key
ENABLE_TRIAL_ACCOUNTS=false
TRIAL_TIME=24 # In hours
# Jellyseerr
JELLYSEERR_ENABLED=false
@@ -21,6 +24,7 @@ JFA_URL=http://localhost:8056
JFA_USERNAME=yourusername
JFA_PASSWORD=yourpassword
JFA_API_KEY=your_api_key_here
JFA_TOKEN=
# QBittorrent
ENABLE_QBITTORRENT=false
@@ -34,6 +38,10 @@ PROXMOX_HOST=https://your-proxmox-server:8006
PROXMOX_TOKEN_NAME=root@pam!yourtokenname
PROXMOX_TOKEN_VALUE=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
PROXMOX_VERIFY_SSL=false
PROXMOX_NODE=pve1
PROXMOX_VM_ID=
# Proxmox type can be "qemu" for VM, "lxc" for container
PROXMOX_TYPE=
# MySQL
DB_HOST=localhost

View File

@@ -1,3 +1,18 @@
# 1.0.9
- You can now configure trial account duration
- Reformatted link command
- Added Updating via command
- Added Backup command
- Added restore command
# 1.0.8
- Fixed update message
- Added changelog command
- Fixed schedule loop for Jfa being enabled when JFA support is disabled
- Added metrics tracking for a Jellyfin container/vm in proxmox
# 1.0.7
- Fixed JFA-GO API keys expiring. The bot now schedules a key refresh

View File

@@ -45,6 +45,7 @@ Fill out values in the .env and you're good to go!
![image](https://cdn.pengucc.com/images/projects/jellycord/readme/role-required.png)
***🎬 User Commands***
- `!createaccount` <username> <password> - Create your Jellyfin account
- `!recoveraccount` <username> <newpassword> - Reset your password
- `!deleteaccount` <username> - Delete your Jellyfin account
@@ -54,9 +55,10 @@ Fill out values in the .env and you're good to go!
- `!help` - Displays help command
***🛠️ Admin Commands***
- `!link` <jellyfin_username> @user - Manually link accounts
- `!link` @user <jellyfin_username> - Manually link accounts
- `!unlink` @user - Manually unlink accounts
- `!listvalidusers` - Show number of valid and invalid accounts
- `!validusers` - Show number of valid and invalid accounts
- `!cleanup` - Remove Jellyfin accounts from users without roles
- `!lastcleanup` - See Last cleanup time, and time remaining before next cleanup
- `!searchaccount` <jellyfin_username> - Find linked Discord user
@@ -65,10 +67,13 @@ Fill out values in the .env and you're good to go!
- `!activestreams` - View all Active Jellyfin streams
***💾 qBittorrent Commands***
- `!qbview` - View current qBittorrent downloads
***🗳️ Proxmox Commands***
- `!storage` - Show available storage pools and free space
- `!metrics` - Show Jellyfin container metrics
***🔑 JFA Commands***
@@ -78,6 +83,12 @@ Fill out values in the .env and you're good to go!
- `!refreshjfakey` - Refreshes the JFA API Key Forcefully
***⚙️ Admin Bot Commands***
- `!setprefix` - Change the bots command prefix
- `!updates` - Manually check for bot updates
- `!update` - Download latest bot version
- `!backup` - Create a backup of the bot and configurations
- `!backups` - List backups of the bot
- `!restore` - Restore a backup of the bot
- `!version` - Manually check for bot updates
- `!changelog` - View changelog for current bot version
- `!logging` - Enable/Disable Console Event Logging

494
app.py
View File

@@ -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 listvalidusers(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 dont 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 dont 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: str = 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 dont 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 dont 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 dont 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 dont 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 dont 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}listvalidusers` - 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()

View File

@@ -6,3 +6,4 @@ pytz==2025.2
apscheduler==3.11.0
qbittorrent-api==2025.7.0
proxmoxer==2.2.0
pymysql==1.1.2

View File

@@ -1 +1 @@
{ "version": "1.0.7" }
{ "version": "1.0.9" }

View File

@@ -1 +1 @@
1.0.7
1.0.9