diff --git a/.env b/.env index 3fa7cab..ff9d3af 100644 --- a/.env +++ b/.env @@ -16,9 +16,16 @@ JELLYSEERR_URL=http://localhost:5055 JELLYSEERR_API_KEY=your_api_key_here # JFA-Go +ENABLE_JFA=false JFA_URL=http://localhost:8056 JFA_API_KEY=your_api_key_here +# QBittorrent +ENABLE_QBITTORRENT=false +QBIT_HOST=http://localhost:8080 +QBIT_USERNAME=your_username +QBIT_PASSWORD=your_password + # MySQL DB_HOST=localhost DB_USER=root diff --git a/CHANGELOG.md b/CHANGELOG.md index 8778ef4..76f79a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ - Added Progress bar to Active Streams - Added JFA-Go support for external invites +- Added qBittorrent Support +- Reformated Help Command # 1.0.5 diff --git a/README.md b/README.md index 2804f2f..0f491f2 100644 --- a/README.md +++ b/README.md @@ -44,24 +44,35 @@ 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*** +***🎬 User Commands*** - `!createaccount` - Create your Jellyfin account - `!recoveraccount` - Reset your password - `!deleteaccount` - Delete your Jellyfin account - `!trialaccount` - Create a 24-hour trial Jellyfin account. Only if ENABLE_TRIAL_ACCOUNTS=True - `!what2watch` - Lists 5 random movie suggestions from the Jellyfin Library +- `!help` - Displays help command -***Admin Commands*** +***πŸ› οΈ Admin Commands*** +- `!link` @user - Manually link accounts +- `!unlink` @user - Manually unlink accounts +- `!listvalidusers` - 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` - Find linked Discord user - `!searchdiscord` @user - Find linked Jellyfin account - `!scanlibraries` - Scan all Jellyfin libraries - `!activestreams` - View all Active Jellyfin streams -- `!link` @user - Manually link accounts -- `!unlink` @user - Manually unlink accounts -***Admin Bot Commands*** +***πŸ’Ύ qBittorrent Commands*** +- `!qbview` - View current qBittorrent downloads + +***πŸ”‘ JFA Commands*** + +- `!createinvite` - Create a new JFA invite link +- `!listinvites` - List all active JFA invite links +- `!deleteinvite ` - Delete a specific JFA Invite + +***βš™οΈ Admin Bot Commands*** - `!setprefix` - Change the bots command prefix - `!updates` - Manually check for bot updates - `!logging` - Enable/Disable Console Event Logging \ No newline at end of file diff --git a/app.py b/app.py index 4c92546..849e269 100644 --- a/app.py +++ b/app.py @@ -7,6 +7,7 @@ import os from dotenv import load_dotenv import pytz import random +import qbittorrentapi # ===================== # ENV + VALIDATION @@ -37,9 +38,15 @@ JELLYSEERR_ENABLED = os.getenv("JELLYSEERR_ENABLED", "false").lower() == "true" JELLYSEERR_URL = os.getenv("JELLYSEERR_URL", "").rstrip("/") JELLYSEERR_API_KEY = os.getenv("JELLYSEERR_API_KEY", "") +ENABLE_JFA = os.getenv("ENABLE_JFA", "False").lower() == "true" JFA_URL = os.getenv("JFA_URL") JFA_API_KEY = os.getenv("JFA_API_KEY") +ENABLE_QBITTORRENT = os.getenv("ENABLE_QBITTORRENT", "False").lower() == "true" +QBIT_HOST = os.getenv("QBIT_HOST") +QBIT_USERNAME = os.getenv("QBIT_USERNAME") +QBIT_PASSWORD = os.getenv("QBIT_PASSWORD") + DB_HOST = get_env_var("DB_HOST") DB_USER = get_env_var("DB_USER") DB_PASSWORD = get_env_var("DB_PASSWORD") @@ -47,7 +54,7 @@ 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.5" +BOT_VERSION = "1.0.6" VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt" RELEASES_URL = "https://github.com/PenguCCN/Jellycord/releases" @@ -70,6 +77,20 @@ intents.members = True intents.message_content = True bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None) +# ===================== +# QBITTORRENT SETUP +# ===================== + +qb = qbittorrentapi.Client( + host=QBIT_HOST, + username=QBIT_USERNAME, + password=QBIT_PASSWORD +) +try: + qb.auth_log_in() +except qbittorrentapi.LoginFailed: + print("Failed to log in to qBittorrent API") + # ===================== # DATABASE SETUP # ===================== @@ -316,6 +337,16 @@ def delete_jellyseerr_user(js_id: str) -> bool: except Exception as e: print(f"[Jellyseerr] Failed to delete user {js_id}: {e}") return False + +# ===================== +# QBITTORRENT HELPERS +# ===================== + +def progress_bar(progress: float, length: int = 20) -> str: + """Return a textual progress bar for the embed.""" + filled_length = int(length * progress) + bar = 'β–ˆ' * filled_length + 'β–‘' * (length - filled_length) + return f"[{bar}] {progress*100:.2f}%" # ===================== # DISCORD HELPERS @@ -487,6 +518,10 @@ async def createaccount(ctx, username: str = None, password: str = None): @bot.command() async def createinvite(ctx): """Admin-only: Create a new JFA-Go invite link (create -> fetch latest invite).""" + if not ENABLE_JFA: + await ctx.send("❌ JFA support is not enabled in the bot configuration.") + return + if not has_admin_role(ctx.author): await ctx.send("❌ You don’t have permission to use this command.") return @@ -574,6 +609,10 @@ async def createinvite(ctx): @bot.command() async def listinvites(ctx): """Admin-only: List all active JFA-Go invites.""" + if not ENABLE_JFA: + await ctx.send("❌ JFA support is not enabled in the bot configuration.") + return + if not has_admin_role(ctx.author): await ctx.send("❌ You don’t have permission to use this command.") return @@ -1130,6 +1169,61 @@ async def activestreams(ctx): await ctx.send(f"❌ Error fetching active streams: {e}") print(f"[activestreams] Error: {e}") +@bot.command() +async def qbview(ctx): + """Admin-only: View current qBittorrent downloads.""" + if not ENABLE_QBITTORRENT: + await ctx.send("❌ qBittorrent support is not enabled in the bot configuration.") + return + + if not has_admin_role(ctx.author): + await ctx.send("❌ You don’t have permission to use this command.") + return + + torrents = qb.torrents_info() + embed = discord.Embed(title="qBittorrent Downloads", color=0x00ff00) + + if not torrents: + embed.description = "No torrents found." + await ctx.send(embed=embed) + return + + # Group torrents by state + state_groups = { + "Downloading / Uploading": [], + "Finished": [], + "Stalled": [], + "Checking / Metadata": [], + "Other": [] + } + + for t in torrents: + if t.state in ("downloading", "uploading"): + state_groups["Downloading / Uploading"].append(t) + elif t.state in ("completed", "pausedUP", "pausedDL"): + state_groups["Finished"].append(t) + elif t.state in ("stalledUP", "stalledDL"): + state_groups["Stalled"].append(t) + elif t.state in ("checkingUP", "checkingDL", "checking", "metaDL"): + state_groups["Checking / Metadata"].append(t) + else: + state_groups["Other"].append(t) + + # Add torrents to embed + for group_name, torrents_list in state_groups.items(): + if torrents_list: + value_text = "" + for torrent in torrents_list: + value_text += ( + f"{torrent.name}\n" + f"{progress_bar(torrent.progress)}\n" + f"Peers: {torrent.num_leechs} | Seeders: {torrent.num_seeds}\n" + f"Status: {torrent.state}\n\n" + ) + embed.add_field(name=group_name, value=value_text, inline=False) + + await ctx.send(embed=embed) + @bot.command() async def link(ctx, jellyfin_username: str = None, user: discord.User = None, js_id: str = None): @@ -1266,47 +1360,71 @@ async def help_command(ctx): color=discord.Color.blue() ) - # User commands + # --- Jellyfin User Commands --- user_cmds = [ f"`{PREFIX}createaccount ` - Create your Jellyfin account", f"`{PREFIX}recoveraccount ` - Reset your password", f"`{PREFIX}deleteaccount ` - Delete your Jellyfin account", f"`{PREFIX}what2watch` - Lists 5 random movie suggestions from the Jellyfin Library" ] - - # Only show trialaccount if enabled if ENABLE_TRIAL_ACCOUNTS: user_cmds.append(f"`{PREFIX}trialaccount ` - Create a 24-hour trial Jellyfin account") + + embed.add_field(name="🎬 Jellyfin Commands", value="\n".join(user_cmds), inline=False) - embed.add_field(name="User Commands", value="\n".join(user_cmds), inline=False) + # --- Bot Commands --- + bot_cmds = [ + f"`{PREFIX}help` - Show this help message" + ] + embed.add_field(name="πŸ€– Bot Commands", value="\n".join(bot_cmds), inline=False) - # Admin commands + # --- Admin Commands --- if is_admin: - # Dynamic link command line + # Admin Jellyfin commands link_command = f"`{PREFIX}link @user` - Manually link accounts" if JELLYSEERR_ENABLED: - link_command = f"`{PREFIX}link @user ` - Manually link accounts with Jellyseerr" + link_command = f"`{PREFIX}link @user ` - Link accounts with Jellyseerr" - embed.add_field(name="Admin Commands", value=( - f"`{PREFIX}cleanup` - Remove Jellyfin accounts from users without roles\n" - f"`{PREFIX}listvalidusers` - Show number of valid and invalid accounts\n" - f"`{PREFIX}lastcleanup` - See Last cleanup time, and time remaining before next cleanup\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" - f"`{PREFIX}activestreams` - View all Active Jellyfin streams\n" - f"{link_command}\n" - f"`{PREFIX}unlink @user` - Manually unlink accounts\n" - ), inline=False) + admin_cmds = [ + link_command, + f"`{PREFIX}unlink @user` - Manually unlink accounts", + f"`{PREFIX}listvalidusers` - 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 ` - Find linked Discord user", + f"`{PREFIX}searchdiscord @user` - Find linked Jellyfin account", + f"`{PREFIX}scanlibraries` - Scan all Jellyfin libraries", + f"`{PREFIX}activestreams` - View all active Jellyfin streams" + ] + embed.add_field(name="πŸ› οΈ Admin Commands", value="\n".join(admin_cmds), inline=False) - embed.add_field(name="Admin Bot Commands", value=( - f"`{PREFIX}setprefix` - Change the bot's command prefix\n" - f"`{PREFIX}updates` - Manually check for bot updates\n" - f"`{PREFIX}logging` - Enable/Disable Console Event Logging\n" - ), inline=False) + # --- qBittorrent Commands --- + if ENABLE_QBITTORRENT: + qb_cmds = [ + f"`{PREFIX}qbview` - Show current qBittorrent downloads with progress, peers, and seeders", + ] + embed.add_field(name="πŸ’Ύ qBittorrent 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 ` - Delete a specific JFA invite" + ] + embed.add_field(name="πŸ”‘ JFA Commands", value="\n".join(jfa_cmds), inline=False) + + # 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}logging` - Enable/disable console event logging" + ] + embed.add_field(name="βš™οΈ Admin Bot Commands", value="\n".join(admin_bot_cmds), inline=False) await ctx.send(embed=embed) + # ===================== # TASKS # ===================== diff --git a/requirements.txt b/requirements.txt index 44f0823..ac7b321 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ requests==2.32.3 mysql-connector-python==9.0.0 python-dotenv==1.0.1 pytz==2025.2 -apscheduler==3.11.0 \ No newline at end of file +apscheduler==3.11.0 +qbittorrent-api==2025.7.0 \ No newline at end of file diff --git a/version.json b/version.json index 49a645b..86ad267 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{ "version": "1.0.5" } \ No newline at end of file +{ "version": "1.0.6" } \ No newline at end of file diff --git a/version.txt b/version.txt index 1464c52..ece61c6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.0.5 \ No newline at end of file +1.0.6 \ No newline at end of file