Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9acbbe5644 | |||
| 2d2e649a5f | |||
| a6f110c9c8 | |||
| 2621fa45e5 | |||
| f1acaf529a | |||
| d970f37343 | |||
| 0ef8fe58d2 | |||
| 24190a3b4a | |||
| 4de1e6e7cb | |||
| 1cc8f7fbaa | |||
| 3727112892 | |||
| 6407c1e41f |
4
.env
4
.env
@@ -8,9 +8,10 @@ REQUIRED_ROLE_IDS=333333333333333333,444444444444444444
|
|||||||
# Jellyfin
|
# Jellyfin
|
||||||
JELLYFIN_URL=http://127.0.0.1:8096
|
JELLYFIN_URL=http://127.0.0.1:8096
|
||||||
JELLYFIN_API_KEY=your_jellyfin_api_key
|
JELLYFIN_API_KEY=your_jellyfin_api_key
|
||||||
|
ENABLE_TRIAL_ACCOUNTS=false
|
||||||
|
|
||||||
# Jellyseerr
|
# Jellyseerr
|
||||||
JELLYSEERR_ENABLED=true
|
JELLYSEERR_ENABLED=false
|
||||||
JELLYSEERR_URL=http://localhost:5055
|
JELLYSEERR_URL=http://localhost:5055
|
||||||
JELLYSEERR_API_KEY=your_api_key_here
|
JELLYSEERR_API_KEY=your_api_key_here
|
||||||
|
|
||||||
@@ -22,3 +23,4 @@ DB_NAME=jellyfin_bot
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
SYNC_LOG_CHANNEL_ID=555555555555555555
|
SYNC_LOG_CHANNEL_ID=555555555555555555
|
||||||
|
EVENT_LOGGING=false
|
||||||
|
|||||||
22
CHANGELOG.md
Normal file
22
CHANGELOG.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 1.0.3
|
||||||
|
|
||||||
|
- Fixed: `ValueError: too many values to unpack (expected 2)`
|
||||||
|
- Cleanup will now delete Jellyseerr accounts as well
|
||||||
|
- Added Trial Jellyfin account support (enable in .env). Will not create a Jellyseerr account, lasts 24 hours, one time use.
|
||||||
|
|
||||||
|
# 1.0.2
|
||||||
|
|
||||||
|
- Fixed Jellyseerr support breaking linking, unlinking, and deletion
|
||||||
|
- Added the ability to link Jellyseerr accounts when linking Jellyfin
|
||||||
|
- Added event logging support for console (Can be toggled in .env or with commands)
|
||||||
|
- Reformatted the updates message
|
||||||
|
- Running a command without any values will now show you the proper command usage
|
||||||
|
|
||||||
|
# 1.0.1
|
||||||
|
|
||||||
|
- Added Jellyseerr Support
|
||||||
|
|
||||||
|
# 1.0.0
|
||||||
|
|
||||||
|
- Bot can now track update releases
|
||||||
|
- Restrict prefixes to non-alphanumeric symbols
|
||||||
22
README.md
22
README.md
@@ -1,4 +1,10 @@
|
|||||||
# Jellyfin-Discord
|
# Jellycord
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[](https://discord.gg/EdPJAhrDq8)
|
||||||
|

|
||||||
|
|
||||||
Allow the creation and management of Jellyfin users via Discord
|
Allow the creation and management of Jellyfin users via Discord
|
||||||
|
|
||||||
Join my [Discord](https://discord.com/invite/zJMUNCPtPy) for help, and keeping an eye out for updates!
|
Join my [Discord](https://discord.com/invite/zJMUNCPtPy) for help, and keeping an eye out for updates!
|
||||||
@@ -7,7 +13,7 @@ This is a very simple and lightweight Jellyfin Discord bot for managing users. I
|
|||||||
|
|
||||||
Fill out values in the .env and you're good to go!
|
Fill out values in the .env and you're good to go!
|
||||||
|
|
||||||
# Features
|
## Features
|
||||||
|
|
||||||
- Automatic Account Cleanup
|
- Automatic Account Cleanup
|
||||||
- Creating Accounts
|
- Creating Accounts
|
||||||
@@ -18,30 +24,31 @@ Fill out values in the .env and you're good to go!
|
|||||||
- Change bot prefix live
|
- Change bot prefix live
|
||||||
- Checks for new releases
|
- Checks for new releases
|
||||||
|
|
||||||
# Command Overview
|
## Command Overview
|
||||||
|
|
||||||
**Pinging the bot will show you the necessary commands to create your account.**
|
**Pinging the bot will show you the necessary commands to create your account.**
|
||||||
|
|
||||||
**PLEASE NOTE BEFORE USING. THIS BOT IS MEANT TO USE REQUIRED ROLES IN ORDER TO WHITELIST USERS FOR JELLYFIN. TAKING A USERS ROLE AWAY WILL DELETE THEIR JELLYFIN ACCOUNT WHEN THE BOT RUNS ITS CLEANUP (24 Hour Schedule or Admin Forced)**
|
**PLEASE NOTE BEFORE USING. THIS BOT IS MEANT TO USE REQUIRED ROLES IN ORDER TO WHITELIST USERS FOR JELLYFIN. TAKING A USERS ROLE AWAY WILL DELETE THEIR JELLYFIN ACCOUNT WHEN THE BOT RUNS ITS CLEANUP (24 Hour Schedule or Admin Forced)**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**There are protections in place to stop users from creating an account where people can see. If a user sends the account creation or reset in a guild, the bot will delete it.**
|
**There are protections in place to stop users from creating an account where people can see. If a user sends the account creation or reset in a guild, the bot will delete it.**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**If a user already has a linked Jellyfin account, the bot will not allow them to create another account.**
|
**If a user already has a linked Jellyfin account, the bot will not allow them to create another account.**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**In order to create an account, you must have the required roles specified in the .env**
|
**In order to create an account, you must have the required roles specified in the .env**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
***User Commands***
|
***User Commands***
|
||||||
- `!createaccount` <username> <password> - Create your Jellyfin account
|
- `!createaccount` <username> <password> - Create your Jellyfin account
|
||||||
- `!recoveraccount` <username> <newpassword> - Reset your password
|
- `!recoveraccount` <username> <newpassword> - Reset your password
|
||||||
- `!deleteaccount` <username> - Delete your Jellyfin account
|
- `!deleteaccount` <username> - Delete your Jellyfin account
|
||||||
|
- `!trialaccount` <username> <password> - Create a 24-hour trial Jellyfin account. Only if ENABLE_TRIAL_ACCOUNTS=True
|
||||||
|
|
||||||
***Admin Commands***
|
***Admin Commands***
|
||||||
- `!cleanup` - Remove Jellyfin accounts from users without roles
|
- `!cleanup` - Remove Jellyfin accounts from users without roles
|
||||||
@@ -55,3 +62,4 @@ Fill out values in the .env and you're good to go!
|
|||||||
***Admin Bot Commands***
|
***Admin Bot Commands***
|
||||||
- `!setprefix` - Change the bots command prefix
|
- `!setprefix` - Change the bots command prefix
|
||||||
- `!updates` - Manually check for bot updates
|
- `!updates` - Manually check for bot updates
|
||||||
|
- `!logging` - Enable/Disable Console Event Logging
|
||||||
534
app.py
534
app.py
@@ -29,6 +29,7 @@ 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")
|
||||||
|
ENABLE_TRIAL_ACCOUNTS = os.getenv("ENABLE_TRIAL_ACCOUNTS", "False").lower() == "true"
|
||||||
|
|
||||||
JELLYSEERR_ENABLED = os.getenv("JELLYSEERR_ENABLED", "false").lower() == "true"
|
JELLYSEERR_ENABLED = os.getenv("JELLYSEERR_ENABLED", "false").lower() == "true"
|
||||||
JELLYSEERR_URL = os.getenv("JELLYSEERR_URL", "").rstrip("/")
|
JELLYSEERR_URL = os.getenv("JELLYSEERR_URL", "").rstrip("/")
|
||||||
@@ -39,9 +40,18 @@ 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.1"
|
BOT_VERSION = "1.0.3"
|
||||||
VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellyfin-Discord/main/version.txt"
|
VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt"
|
||||||
RELEASES_URL = "https://github.com/PenguCCN/Jellyfin-Discord/releases"
|
RELEASES_URL = "https://github.com/PenguCCN/Jellycord/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
|
||||||
@@ -55,25 +65,28 @@ bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None)
|
|||||||
# DATABASE SETUP
|
# DATABASE SETUP
|
||||||
# =====================
|
# =====================
|
||||||
def init_db():
|
def init_db():
|
||||||
# Create database if it doesn't exist
|
log_event(f"Initiating Database...")
|
||||||
conn = mysql.connector.connect(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()
|
||||||
|
|
||||||
# Connect to the database
|
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()
|
||||||
|
|
||||||
# Create accounts table if it doesn't exist
|
# Normal accounts table
|
||||||
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,
|
jellyfin_id VARCHAR(255) NOT NULL,
|
||||||
jellyseerr_id VARCHAR(255) DEFAULT NULL
|
jellyseerr_id VARCHAR(255)
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -89,7 +102,18 @@ def init_db():
|
|||||||
cur.execute("ALTER TABLE accounts ADD COLUMN jellyseerr_id VARCHAR(255) DEFAULT NULL")
|
cur.execute("ALTER TABLE accounts ADD COLUMN jellyseerr_id VARCHAR(255) DEFAULT NULL")
|
||||||
print("[DB] Added missing column 'jellyseerr_id' to accounts table.")
|
print("[DB] Added missing column 'jellyseerr_id' to accounts table.")
|
||||||
|
|
||||||
# Create bot_metadata table if it doesn't exist
|
# Trial accounts table (persistent history, one-time only)
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS trial_accounts (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
discord_id BIGINT NOT NULL UNIQUE,
|
||||||
|
jellyfin_username VARCHAR(255),
|
||||||
|
jellyfin_id VARCHAR(255),
|
||||||
|
trial_created_at DATETIME NOT NULL,
|
||||||
|
expired BOOLEAN DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
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,
|
||||||
@@ -97,6 +121,14 @@ def init_db():
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Cleanup logs table
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS cleanup_logs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
run_at DATETIME NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -115,6 +147,25 @@ def add_account(discord_id, username, jf_id, js_id=None):
|
|||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def init_trial_accounts_table():
|
||||||
|
conn = mysql.connector.connect(
|
||||||
|
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
# Persistent trial accounts table
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS trial_accounts (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
discord_id BIGINT NOT NULL UNIQUE,
|
||||||
|
jellyfin_username VARCHAR(255) NOT NULL,
|
||||||
|
jellyfin_id VARCHAR(255) NOT NULL,
|
||||||
|
trial_created_at DATETIME NOT NULL,
|
||||||
|
expired BOOLEAN DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def get_accounts():
|
def get_accounts():
|
||||||
@@ -122,22 +173,24 @@ def get_accounts():
|
|||||||
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(
|
||||||
@@ -151,7 +204,7 @@ def get_account_by_discord(discord_id):
|
|||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
return row # (jellyfin_username, jellyfin_id, jellyseerr_id)
|
return row # (jellyfin_username, jf_id, js_id)
|
||||||
|
|
||||||
|
|
||||||
def delete_account(discord_id):
|
def delete_account(discord_id):
|
||||||
@@ -224,26 +277,35 @@ def import_jellyseerr_user(jellyfin_user_id: str) -> str:
|
|||||||
print(f"[Jellyseerr] Failed to import user: {e}")
|
print(f"[Jellyseerr] Failed to import user: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_jellyseerr_id(jf_id: str) -> str | None:
|
||||||
|
"""Return the Jellyseerr user ID for a given Jellyfin user ID."""
|
||||||
def delete_jellyseerr_user(username: str) -> bool:
|
|
||||||
if not JELLYSEERR_ENABLED:
|
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
|
return True
|
||||||
headers = {"X-Api-Key": JELLYSEERR_API_KEY}
|
headers = {"X-Api-Key": JELLYSEERR_API_KEY}
|
||||||
try:
|
try:
|
||||||
# First fetch users to find matching ID
|
dr = requests.delete(f"{JELLYSEERR_URL}/api/v1/user/{js_id}", headers=headers, timeout=10)
|
||||||
r = requests.get(f"{JELLYSEERR_URL}/api/v1/user", headers=headers, timeout=10)
|
return dr.status_code in (200, 204)
|
||||||
if r.status_code != 200:
|
|
||||||
return False
|
|
||||||
users = r.json()
|
|
||||||
for u in users:
|
|
||||||
if u.get("username", "").lower() == username.lower():
|
|
||||||
user_id = u["id"]
|
|
||||||
dr = requests.delete(f"{JELLYSEERR_URL}/api/v1/user/{user_id}", headers=headers, timeout=10)
|
|
||||||
return dr.status_code in (200, 204)
|
|
||||||
return True # no user found, nothing to delete
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Jellyseerr] Failed to delete user {username}: {e}")
|
print(f"[Jellyseerr] Failed to delete user {js_id}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
@@ -282,6 +344,33 @@ def get_metadata(key):
|
|||||||
conn.close()
|
conn.close()
|
||||||
return row[0] if row else None
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# EVENTS
|
# EVENTS
|
||||||
# =====================
|
# =====================
|
||||||
@@ -306,20 +395,31 @@ 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):
|
||||||
# DM-only
|
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):
|
||||||
try:
|
try: await ctx.message.delete()
|
||||||
await ctx.message.delete()
|
except discord.Forbidden: pass
|
||||||
except discord.Forbidden:
|
|
||||||
pass
|
|
||||||
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to create your Jellyfin account.")
|
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) if guild else None
|
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(f"❌ {ctx.author.mention}, you don’t have the required role.")
|
await ctx.send(f"❌ {ctx.author.mention}, you don’t have the required role.")
|
||||||
return
|
return
|
||||||
@@ -328,7 +428,6 @@ async def createaccount(ctx, username: str, password: str):
|
|||||||
await ctx.send(f"❌ {ctx.author.mention}, you already have a Jellyfin account.")
|
await ctx.send(f"❌ {ctx.author.mention}, you already have a Jellyfin account.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create Jellyfin user
|
|
||||||
if create_jellyfin_user(username, password):
|
if create_jellyfin_user(username, password):
|
||||||
jf_id = get_jellyfin_user(username)
|
jf_id = get_jellyfin_user(username)
|
||||||
if not jf_id:
|
if not jf_id:
|
||||||
@@ -336,89 +435,148 @@ async def createaccount(ctx, username: str, password: str):
|
|||||||
return
|
return
|
||||||
|
|
||||||
js_id = None
|
js_id = None
|
||||||
# Import to Jellyseerr if enabled
|
|
||||||
if JELLYSEERR_ENABLED:
|
if JELLYSEERR_ENABLED:
|
||||||
js_id = import_jellyseerr_user(jf_id)
|
js_id = import_jellyseerr_user(jf_id)
|
||||||
|
|
||||||
# Store account in DB
|
|
||||||
add_account(ctx.author.id, username, jf_id, js_id)
|
add_account(ctx.author.id, username, jf_id, js_id)
|
||||||
|
|
||||||
if JELLYSEERR_ENABLED:
|
if JELLYSEERR_ENABLED:
|
||||||
if js_id:
|
if js_id:
|
||||||
await ctx.send(
|
await ctx.send(f"✅ Jellyfin account **{username}** created and imported into Jellyseerr!\n🌐 Login here: {JELLYFIN_URL}")
|
||||||
f"✅ Jellyfin account **{username}** created and imported into Jellyseerr!\n"
|
|
||||||
f"🌐 Login here: {JELLYFIN_URL}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(f"⚠️ Jellyfin account **{username}** created, but Jellyseerr import failed.\n🌐 Login here: {JELLYFIN_URL}")
|
||||||
f"⚠️ Jellyfin account **{username}** created, but Jellyseerr import failed.\n"
|
|
||||||
f"🌐 Login here: {JELLYFIN_URL}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(f"✅ Jellyfin account **{username}** created!\n🌐 Login here: {JELLYFIN_URL}")
|
await ctx.send(f"✅ Jellyfin account **{username}** created!\n🌐 Login here: {JELLYFIN_URL}")
|
||||||
else:
|
else:
|
||||||
await ctx.send(f"❌ Failed to create Jellyfin account **{username}**. It 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 trialaccount(ctx, username: str = None, password: str = None):
|
||||||
"""DM-only: reset your Jellyfin password"""
|
"""Create a 24-hour trial Jellyfin account. DM-only, one-time per user."""
|
||||||
|
log_event(f"trialaccount invoked by {ctx.author}")
|
||||||
|
|
||||||
|
# Ensure trial accounts are enabled
|
||||||
|
if not ENABLE_TRIAL_ACCOUNTS:
|
||||||
|
await ctx.send("❌ Trial accounts are currently disabled.")
|
||||||
|
return
|
||||||
|
|
||||||
# Ensure it's a DM
|
# Ensure it's a DM
|
||||||
if not isinstance(ctx.channel, discord.DMChannel):
|
|
||||||
await ctx.message.delete()
|
|
||||||
await ctx.send(f"{ctx.author.mention} Please DM me to reset your password.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Fetch the Jellyfin account linked to this Discord user
|
|
||||||
acc = get_account_by_discord(ctx.author.id)
|
|
||||||
if not acc:
|
|
||||||
await ctx.send("❌ You do not have a linked Jellyfin account.")
|
|
||||||
return
|
|
||||||
|
|
||||||
username = acc[0] # the Jellyfin username
|
|
||||||
|
|
||||||
# Reset the password
|
|
||||||
if reset_jellyfin_password(username, new_password):
|
|
||||||
await ctx.send(
|
|
||||||
f"✅ Your Jellyfin password for **{username}** has been reset!\n"
|
|
||||||
f"🌐 Login here: {JELLYFIN_URL}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await ctx.send(f"❌ Failed to reset password for **{username}**. Please contact an admin.")
|
|
||||||
|
|
||||||
@bot.command()
|
|
||||||
async def deleteaccount(ctx, username: str):
|
|
||||||
if not isinstance(ctx.channel, discord.DMChannel):
|
if not isinstance(ctx.channel, discord.DMChannel):
|
||||||
try:
|
try:
|
||||||
await ctx.message.delete()
|
await ctx.message.delete()
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
pass
|
pass
|
||||||
|
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to create a trial account.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure required arguments
|
||||||
|
if username is None or password is None:
|
||||||
|
await ctx.send(command_usage(f"{PREFIX}trialaccount", ["<username>", "<password>"]))
|
||||||
|
return
|
||||||
|
|
||||||
|
guild = bot.get_guild(GUILD_ID)
|
||||||
|
member = guild.get_member(ctx.author.id) if guild else None
|
||||||
|
|
||||||
|
# Check required server role
|
||||||
|
if not member or not has_required_role(member):
|
||||||
|
await ctx.send(f"❌ {ctx.author.mention}, you don’t have the required role.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if user already has a normal Jellyfin account
|
||||||
|
if get_account_by_discord(ctx.author.id):
|
||||||
|
await ctx.send(f"❌ {ctx.author.mention}, you already have a Jellyfin account.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if user already had a trial account (one-time)
|
||||||
|
conn = mysql.connector.connect(
|
||||||
|
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT * FROM trial_accounts WHERE discord_id=%s", (ctx.author.id,))
|
||||||
|
existing_trial = cur.fetchone()
|
||||||
|
if existing_trial:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
await ctx.send(f"❌ {ctx.author.mention}, you have already used your trial account. You cannot create another.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create Jellyfin trial user
|
||||||
|
if create_jellyfin_user(username, password):
|
||||||
|
jf_id = get_jellyfin_user(username)
|
||||||
|
if not jf_id:
|
||||||
|
await ctx.send(f"❌ Failed to fetch Jellyfin ID for **{username}**. Please contact an admin.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Store trial account info in separate persistent table
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO trial_accounts (discord_id, jellyfin_username, jellyfin_id, trial_created_at, expired)
|
||||||
|
VALUES (%s, %s, %s, NOW(), 0)
|
||||||
|
""", (ctx.author.id, username, jf_id))
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
await ctx.send(f"✅ Trial Jellyfin account **{username}** created! It will expire in 24 hours.\n🌐 Login here: {JELLYFIN_URL}")
|
||||||
|
log_event(f"Trial account created for {ctx.author} ({username})")
|
||||||
|
else:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
await ctx.send(f"❌ Failed to create trial account **{username}**. It may already exist.")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def recoveraccount(ctx, new_password: str = None):
|
||||||
|
log_event(f"recoveraccount invoked by {ctx.author}")
|
||||||
|
if new_password is None:
|
||||||
|
await ctx.send(command_usage(f"{PREFIX}recoveraccount", ["<newpassword>"]))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isinstance(ctx.channel, discord.DMChannel):
|
||||||
|
await ctx.message.delete()
|
||||||
|
await ctx.send(f"{ctx.author.mention} Please DM me to reset your password.")
|
||||||
|
return
|
||||||
|
|
||||||
|
acc = get_account_by_discord(ctx.author.id)
|
||||||
|
if not acc:
|
||||||
|
await ctx.send("❌ You do not have a linked Jellyfin account.")
|
||||||
|
return
|
||||||
|
|
||||||
|
username = acc[0]
|
||||||
|
if reset_jellyfin_password(username, new_password):
|
||||||
|
await ctx.send(f"✅ Your Jellyfin password for **{username}** has been reset!\n🌐 Login here: {JELLYFIN_URL}")
|
||||||
|
else:
|
||||||
|
await ctx.send(f"❌ Failed to reset password for **{username}**. Please contact an admin.")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
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):
|
||||||
|
try: await ctx.message.delete()
|
||||||
|
except discord.Forbidden: pass
|
||||||
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to delete your Jellyfin account.")
|
await ctx.send(f"{ctx.author.mention} ❌ Please DM me to delete your Jellyfin account.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fetch account linked to this Discord user
|
|
||||||
acc = get_account_by_discord(ctx.author.id)
|
acc = get_account_by_discord(ctx.author.id)
|
||||||
if not acc or acc[0].lower() != username.lower():
|
if not acc or acc[0].lower() != username.lower():
|
||||||
await ctx.send(f"❌ {ctx.author.mention}, that Jellyfin account is not linked to you.")
|
await ctx.send(f"❌ {ctx.author.mention}, that Jellyfin account is not linked to you.")
|
||||||
return
|
return
|
||||||
|
|
||||||
jf_id = acc[1] # Jellyfin ID
|
jf_id, js_id = acc[1], acc[2] if len(acc) > 2 else None
|
||||||
js_id = acc[2] if len(acc) > 2 else None # Jellyseerr ID
|
|
||||||
|
|
||||||
# Delete Jellyfin account
|
|
||||||
if delete_jellyfin_user(username):
|
if delete_jellyfin_user(username):
|
||||||
delete_account(ctx.author.id)
|
delete_account(ctx.author.id)
|
||||||
|
|
||||||
# Delete Jellyseerr user if enabled
|
|
||||||
if JELLYSEERR_ENABLED and js_id:
|
if JELLYSEERR_ENABLED and js_id:
|
||||||
try:
|
try:
|
||||||
headers = {"X-Api-Key": JELLYSEERR_API_KEY}
|
headers = {"X-Api-Key": JELLYSEERR_API_KEY}
|
||||||
dr = requests.delete(f"{JELLYSEERR_URL}/api/v1/user/{js_id}", headers=headers, timeout=10)
|
dr = requests.delete(f"{JELLYSEERR_URL}/api/v1/user/{js_id}", headers=headers, timeout=10)
|
||||||
if dr.status_code in (200, 204):
|
if dr.status_code in (200, 204): print(f"[Jellyseerr] User {js_id} removed successfully.")
|
||||||
print(f"[Jellyseerr] User {js_id} removed successfully.")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Jellyseerr] Failed to delete user {js_id}: {e}")
|
print(f"[Jellyseerr] Failed to delete user {js_id}: {e}")
|
||||||
|
|
||||||
await ctx.send(f"✅ Jellyfin account **{username}** deleted successfully.")
|
await ctx.send(f"✅ Jellyfin account **{username}** deleted successfully.")
|
||||||
else:
|
else:
|
||||||
await ctx.send(f"❌ Failed to delete Jellyfin account **{username}**.")
|
await ctx.send(f"❌ Failed to delete Jellyfin account **{username}**.")
|
||||||
@@ -426,13 +584,30 @@ async def deleteaccount(ctx, username: str):
|
|||||||
|
|
||||||
@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 row in get_accounts():
|
||||||
|
discord_id = row[0]
|
||||||
|
jf_username = row[1]
|
||||||
|
jf_id = row[2] if len(row) > 2 else None
|
||||||
|
js_id = row[3] if len(row) > 3 else None
|
||||||
|
|
||||||
m = guild.get_member(discord_id)
|
m = guild.get_member(discord_id)
|
||||||
if m is None or not has_required_role(m):
|
if m is None or not has_required_role(m):
|
||||||
if delete_jellyfin_user(jf_username):
|
if delete_jellyfin_user(jf_username):
|
||||||
delete_account(discord_id)
|
delete_account(discord_id)
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
removed.append(jf_username)
|
removed.append(jf_username)
|
||||||
|
|
||||||
log_channel = bot.get_channel(SYNC_LOG_CHANNEL_ID)
|
log_channel = bot.get_channel(SYNC_LOG_CHANNEL_ID)
|
||||||
@@ -441,8 +616,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.")
|
||||||
@@ -461,17 +638,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)
|
||||||
@@ -482,11 +656,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)
|
||||||
@@ -495,52 +670,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.send("❌ You don’t have permission to use this command.")
|
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):
|
||||||
|
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.")
|
||||||
@@ -550,12 +742,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:
|
||||||
@@ -563,13 +752,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.")
|
||||||
@@ -579,20 +769,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)
|
||||||
|
|
||||||
@@ -602,12 +821,20 @@ async def help_command(ctx):
|
|||||||
color=discord.Color.blue()
|
color=discord.Color.blue()
|
||||||
)
|
)
|
||||||
|
|
||||||
embed.add_field(name="User Commands", value=(
|
# User commands
|
||||||
f"`{PREFIX}createaccount <username> <password>` - Create your Jellyfin account\n"
|
user_cmds = [
|
||||||
f"`{PREFIX}recoveraccount <newpassword>` - Reset your password\n"
|
f"`{PREFIX}createaccount <username> <password>` - Create your Jellyfin account",
|
||||||
f"`{PREFIX}deleteaccount <username>` - Delete your Jellyfin account\n"
|
f"`{PREFIX}recoveraccount <newpassword>` - Reset your password",
|
||||||
), inline=False)
|
f"`{PREFIX}deleteaccount <username>` - Delete your Jellyfin account"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Only show trialaccount if enabled
|
||||||
|
if ENABLE_TRIAL_ACCOUNTS:
|
||||||
|
user_cmds.append(f"`{PREFIX}trialaccount <username> <password>` - Create a 24-hour trial Jellyfin account")
|
||||||
|
|
||||||
|
embed.add_field(name="User Commands", value="\n".join(user_cmds), inline=False)
|
||||||
|
|
||||||
|
# Admin commands
|
||||||
if is_admin:
|
if is_admin:
|
||||||
embed.add_field(name="Admin Commands", value=(
|
embed.add_field(name="Admin Commands", value=(
|
||||||
f"`{PREFIX}cleanup` - Remove Jellyfin accounts from users without roles\n"
|
f"`{PREFIX}cleanup` - Remove Jellyfin accounts from users without roles\n"
|
||||||
@@ -619,8 +846,9 @@ async def help_command(ctx):
|
|||||||
f"`{PREFIX}unlink @user` - Manually unlink accounts\n"
|
f"`{PREFIX}unlink @user` - Manually unlink accounts\n"
|
||||||
), inline=False)
|
), inline=False)
|
||||||
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 bot's 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)
|
||||||
@@ -635,18 +863,49 @@ async def daily_check():
|
|||||||
guild = bot.get_guild(GUILD_ID)
|
guild = bot.get_guild(GUILD_ID)
|
||||||
removed = []
|
removed = []
|
||||||
|
|
||||||
for discord_id, jf_username in get_accounts():
|
# Normal accounts cleanup
|
||||||
|
for discord_id, jf_username, jf_id, js_id in get_accounts():
|
||||||
m = guild.get_member(discord_id)
|
m = guild.get_member(discord_id)
|
||||||
if m is None or not has_required_role(m):
|
if m is None or not has_required_role(m):
|
||||||
if delete_jellyfin_user(jf_username):
|
if delete_jellyfin_user(jf_username):
|
||||||
delete_account(discord_id)
|
delete_account(discord_id)
|
||||||
removed.append(jf_username)
|
removed.append(jf_username)
|
||||||
|
|
||||||
if removed:
|
# Trial accounts cleanup
|
||||||
print(f"Daily cleanup: removed {len(removed)} accounts: {removed}")
|
conn = mysql.connector.connect(
|
||||||
|
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
|
||||||
|
)
|
||||||
|
cur = conn.cursor(dictionary=True)
|
||||||
|
cur.execute("SELECT * FROM trial_accounts WHERE expired=0")
|
||||||
|
trials = cur.fetchall()
|
||||||
|
|
||||||
|
for trial in trials:
|
||||||
|
created_at = trial["trial_created_at"]
|
||||||
|
if created_at and datetime.datetime.utcnow() > created_at + datetime.timedelta(hours=24):
|
||||||
|
# Delete from Jellyfin
|
||||||
|
delete_jellyfin_user(trial["jellyfin_username"])
|
||||||
|
# Mark trial as expired
|
||||||
|
cur.execute("UPDATE trial_accounts SET expired=1 WHERE discord_id=%s", (trial["discord_id"],))
|
||||||
|
conn.commit()
|
||||||
|
removed.append(f"{trial['jellyfin_username']} (trial)")
|
||||||
|
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Record cleanup run
|
||||||
|
conn = mysql.connector.connect(
|
||||||
|
host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("INSERT INTO cleanup_logs (run_at) VALUES (%s)", (datetime.datetime.utcnow(),))
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
print(f"Cleanup removed {len(removed)} accounts: {removed}")
|
||||||
|
|
||||||
# Log last run timestamp
|
|
||||||
set_metadata("last_cleanup", datetime.datetime.utcnow().isoformat())
|
|
||||||
|
|
||||||
@tasks.loop(hours=1)
|
@tasks.loop(hours=1)
|
||||||
async def check_for_updates():
|
async def check_for_updates():
|
||||||
@@ -658,11 +917,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}")
|
||||||
|
|
||||||
|
|||||||
1
version.json
Normal file
1
version.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{ "version": "1.0.3" }
|
||||||
@@ -1 +1 @@
|
|||||||
1.0.1
|
1.0.3
|
||||||
Reference in New Issue
Block a user