diff --git a/README.md b/README.md index a0b1ffb..8d48fd0 100644 --- a/README.md +++ b/README.md @@ -38,16 +38,16 @@ Fill out values in the .env and you're good to go! ![image](https://cdn.pengucc.com/images/projects/jellyfin-bot/role-required.png) ***User Commands*** -- !createaccount - Create your Jellyfin account -- !recoveraccount - Reset your password -- !deleteaccount - Delete your Jellyfin account +- `!createaccount` - Create your Jellyfin account +- `!recoveraccount` - Reset your password +- `!deleteaccount` - Delete your Jellyfin account ***Admin Commands*** -- !syncaccounts - Remove Jellyfin accounts from users without roles -- !searchaccount - Find linked Discord user -- !searchdiscord @user - Find linked Jellyfin account -- !scanlibraries - Scan all Jellyfin libraries -- !assignaccount @user - Manually link accounts +- `!cleanup` - Remove Jellyfin accounts from users without roles +- `!searchaccount` - Find linked Discord user +- `!searchdiscord` @user - Find linked Jellyfin account +- `!scanlibraries` - Scan all Jellyfin libraries +- `!assignaccount` @user - Manually link accounts ***Admin Bot Commands*** -- !setprefix - Change the bots command prefix \ No newline at end of file +- `!setprefix` - Change the bots command prefix \ No newline at end of file diff --git a/app.py b/app.py index 1d8cd90..22ad7d1 100644 --- a/app.py +++ b/app.py @@ -47,6 +47,7 @@ bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None) # DATABASE SETUP # ===================== def init_db(): + # Existing DB creation conn = mysql.connector.connect( host=DB_HOST, user=DB_USER, password=DB_PASSWORD ) @@ -66,6 +67,13 @@ def init_db(): jellyfin_username VARCHAR(255) NOT NULL ) """) + # New table for metadata + cur.execute(""" + CREATE TABLE IF NOT EXISTS bot_metadata ( + key_name VARCHAR(255) PRIMARY KEY, + value VARCHAR(255) NOT NULL + ) + """) conn.commit() cur.close() conn.close() @@ -168,6 +176,33 @@ def has_required_role(member): def has_admin_role(member): return any(role.id in ADMIN_ROLE_IDS for role in member.roles) +# ===================== +# BOT HELPERS +# ===================== + +def set_metadata(key, value): + conn = mysql.connector.connect( + host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME + ) + cur = conn.cursor() + cur.execute(""" + REPLACE INTO bot_metadata (key_name, value) VALUES (%s, %s) + """, (key, str(value))) + conn.commit() + cur.close() + conn.close() + +def get_metadata(key): + conn = mysql.connector.connect( + host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME + ) + cur = conn.cursor() + cur.execute("SELECT value FROM bot_metadata WHERE key_name=%s", (key,)) + row = cur.fetchone() + cur.close() + conn.close() + return row[0] if row else None + # ===================== # EVENTS # ===================== @@ -261,7 +296,7 @@ async def deleteaccount(ctx, username: str): await ctx.send("❌ Failed to delete account.") @bot.command() -async def syncaccounts(ctx): +async def cleanup(ctx): guild = bot.get_guild(GUILD_ID) removed = [] for discord_id, jf_username in get_accounts(): @@ -275,7 +310,33 @@ async def syncaccounts(ctx): if removed and log_channel: await log_channel.send(f"🧹 Removed {len(removed)} Jellyfin accounts: {', '.join(removed)}") - await ctx.send("✅ Sync complete.") + await ctx.send("✅ Cleanup complete.") + +@bot.command() +async def lastcleanup(ctx): + member = ctx.guild.get_member(ctx.author.id) + if not has_admin_role(member): + await ctx.send("❌ You don’t have permission to view the last cleanup.") + return + + last_run = get_metadata("last_cleanup") + if not last_run: + await ctx.send("ℹ️ No cleanup has been run yet.") + return + + last_run_dt = datetime.datetime.fromisoformat(last_run) + now = datetime.datetime.utcnow() + next_run_dt = last_run_dt + datetime.timedelta(hours=24) + time_remaining = next_run_dt - now + + hours, remainder = divmod(int(time_remaining.total_seconds()), 3600) + minutes, seconds = divmod(remainder, 60) + + await ctx.send( + 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() async def searchaccount(ctx, username: str): @@ -374,7 +435,7 @@ async def help_command(ctx): if is_admin: embed.add_field(name="Admin Commands", value=( - f"`{PREFIX}syncaccounts` - Remove Jellyfin accounts from users without roles\n" + f"`{PREFIX}cleanup` - Remove Jellyfin accounts from users without roles\n" f"`{PREFIX}searchaccount ` - Find linked Discord user\n" f"`{PREFIX}searchdiscord @user` - Find linked Jellyfin account\n" f"`{PREFIX}scanlibraries` - Scan all Jellyfin libraries\n" @@ -389,23 +450,42 @@ async def help_command(ctx): # ===================== # TASKS # ===================== +import datetime + @tasks.loop(hours=24) async def daily_check(): guild = bot.get_guild(GUILD_ID) removed = [] + for discord_id, jf_username in get_accounts(): m = guild.get_member(discord_id) if m is None or not has_required_role(m): if delete_jellyfin_user(jf_username): delete_account(discord_id) removed.append(jf_username) + if removed: print(f"Daily cleanup: removed {len(removed)} accounts: {removed}") + # Log last run timestamp + set_metadata("last_cleanup", datetime.datetime.utcnow().isoformat()) + + @bot.event async def on_ready(): print(f"Logged in as {bot.user}") init_db() + + # Check last cleanup + last_run = get_metadata("last_cleanup") + if last_run: + last_run_dt = datetime.datetime.fromisoformat(last_run) + now = datetime.datetime.utcnow() + delta = now - last_run_dt + if delta.total_seconds() >= 24 * 3600: + print("Running missed daily cleanup...") + await daily_check() # Run immediately if overdue + daily_check.start() await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=f"{PREFIX}help"))