Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fe3d2e5d1 | |||
| e0bbbd02f5 | |||
| d80746cd61 | |||
| f6197c144a | |||
| 2481ba778b | |||
| c5d619d450 | |||
| 547ccbbb0a | |||
| f862f46e16 | |||
| c4a1ccf770 | |||
| a050becc14 |
14
.env
14
.env
@@ -18,7 +18,10 @@ JELLYSEERR_API_KEY=your_api_key_here
|
|||||||
# JFA-Go
|
# JFA-Go
|
||||||
ENABLE_JFA=false
|
ENABLE_JFA=false
|
||||||
JFA_URL=http://localhost:8056
|
JFA_URL=http://localhost:8056
|
||||||
|
JFA_USERNAME=yourusername
|
||||||
|
JFA_PASSWORD=yourpassword
|
||||||
JFA_API_KEY=your_api_key_here
|
JFA_API_KEY=your_api_key_here
|
||||||
|
JFA_TOKEN=
|
||||||
|
|
||||||
# QBittorrent
|
# QBittorrent
|
||||||
ENABLE_QBITTORRENT=false
|
ENABLE_QBITTORRENT=false
|
||||||
@@ -26,6 +29,17 @@ QBIT_HOST=http://localhost:8080
|
|||||||
QBIT_USERNAME=your_username
|
QBIT_USERNAME=your_username
|
||||||
QBIT_PASSWORD=your_password
|
QBIT_PASSWORD=your_password
|
||||||
|
|
||||||
|
# Proxmox
|
||||||
|
ENABLE_PROXMOX=false
|
||||||
|
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
|
# MySQL
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_USER=root
|
DB_USER=root
|
||||||
|
|||||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
|||||||
|
# 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
|
||||||
|
- Added Proxmox support for checking storage pool size
|
||||||
|
- !what2watch is now !movies2watch
|
||||||
|
- Added !shows2watch, now pulls 5 random shows
|
||||||
|
|
||||||
# 1.0.6
|
# 1.0.6
|
||||||
|
|
||||||
- Added Progress bar to Active Streams
|
- Added Progress bar to Active Streams
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -45,14 +45,17 @@ Fill out values in the .env and you're good to go!
|
|||||||

|

|
||||||
|
|
||||||
***🎬 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
|
- `!trialaccount` <username> <password> - Create a 24-hour trial Jellyfin account. Only if ENABLE_TRIAL_ACCOUNTS=True
|
||||||
- `!what2watch` - Lists 5 random movie suggestions from the Jellyfin Library
|
- `!movies2watch` - Lists 5 random movie suggestions from the Jellyfin Library
|
||||||
|
- `!shows2watch` - Lists 5 random show suggestions from the Jellyfin Library
|
||||||
- `!help` - Displays help command
|
- `!help` - Displays help command
|
||||||
|
|
||||||
***🛠️ Admin Commands***
|
***🛠️ Admin Commands***
|
||||||
|
|
||||||
- `!link` <jellyfin_username> @user - Manually link accounts
|
- `!link` <jellyfin_username> @user - Manually link accounts
|
||||||
- `!unlink` @user - Manually unlink accounts
|
- `!unlink` @user - Manually unlink accounts
|
||||||
- `!listvalidusers` - Show number of valid and invalid accounts
|
- `!listvalidusers` - Show number of valid and invalid accounts
|
||||||
@@ -64,15 +67,24 @@ Fill out values in the .env and you're good to go!
|
|||||||
- `!activestreams` - View all Active Jellyfin streams
|
- `!activestreams` - View all Active Jellyfin streams
|
||||||
|
|
||||||
***💾 qBittorrent Commands***
|
***💾 qBittorrent Commands***
|
||||||
|
|
||||||
- `!qbview` - View current qBittorrent downloads
|
- `!qbview` - View current qBittorrent downloads
|
||||||
|
|
||||||
|
***🗳️ Proxmox Commands***
|
||||||
|
|
||||||
|
- `!storage` - Show available storage pools and free space
|
||||||
|
- `!metrics` - Show Jellyfin container metrics
|
||||||
|
|
||||||
***🔑 JFA Commands***
|
***🔑 JFA Commands***
|
||||||
|
|
||||||
- `!createinvite` - Create a new JFA invite link
|
- `!createinvite` - Create a new JFA invite link
|
||||||
- `!listinvites` - List all active JFA invite links
|
- `!listinvites` - List all active JFA invite links
|
||||||
- `!deleteinvite <code>` - Delete a specific JFA Invite
|
- `!deleteinvite <code>` - Delete a specific JFA Invite
|
||||||
|
- `!refreshjfakey` - Refreshes the JFA API Key Forcefully
|
||||||
|
|
||||||
***⚙️ 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
|
||||||
|
- `!changelog` - View changelog for current bot version
|
||||||
- `!logging` - Enable/Disable Console Event Logging
|
- `!logging` - Enable/Disable Console Event Logging
|
||||||
7
ROADMAP.md
Normal file
7
ROADMAP.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
- **Future Features**
|
||||||
|
|
||||||
|
- Servarr Support
|
||||||
|
|
||||||
|
- **To Do**
|
||||||
|
|
||||||
|
- Music/Podcast Support (Listening in VC from Jellyfin Library)
|
||||||
531
app.py
531
app.py
@@ -8,6 +8,7 @@ from dotenv import load_dotenv
|
|||||||
import pytz
|
import pytz
|
||||||
import random
|
import random
|
||||||
import qbittorrentapi
|
import qbittorrentapi
|
||||||
|
from proxmoxer import ProxmoxAPI
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# ENV + VALIDATION
|
# ENV + VALIDATION
|
||||||
@@ -40,6 +41,8 @@ JELLYSEERR_API_KEY = os.getenv("JELLYSEERR_API_KEY", "")
|
|||||||
|
|
||||||
ENABLE_JFA = os.getenv("ENABLE_JFA", "False").lower() == "true"
|
ENABLE_JFA = os.getenv("ENABLE_JFA", "False").lower() == "true"
|
||||||
JFA_URL = os.getenv("JFA_URL")
|
JFA_URL = os.getenv("JFA_URL")
|
||||||
|
JFA_USERNAME = os.getenv("JFA_USERNAME")
|
||||||
|
JFA_PASSWORD = os.getenv("JFA_PASSWORD")
|
||||||
JFA_API_KEY = os.getenv("JFA_API_KEY")
|
JFA_API_KEY = os.getenv("JFA_API_KEY")
|
||||||
|
|
||||||
ENABLE_QBITTORRENT = os.getenv("ENABLE_QBITTORRENT", "False").lower() == "true"
|
ENABLE_QBITTORRENT = os.getenv("ENABLE_QBITTORRENT", "False").lower() == "true"
|
||||||
@@ -47,6 +50,15 @@ QBIT_HOST = os.getenv("QBIT_HOST")
|
|||||||
QBIT_USERNAME = os.getenv("QBIT_USERNAME")
|
QBIT_USERNAME = os.getenv("QBIT_USERNAME")
|
||||||
QBIT_PASSWORD = os.getenv("QBIT_PASSWORD")
|
QBIT_PASSWORD = os.getenv("QBIT_PASSWORD")
|
||||||
|
|
||||||
|
ENABLE_PROXMOX = os.getenv("ENABLE_PROXMOX", "False").lower() == "true"
|
||||||
|
PROXMOX_HOST = os.getenv("PROXMOX_HOST")
|
||||||
|
PROXMOX_TOKEN_NAME = os.getenv("PROXMOX_TOKEN_NAME")
|
||||||
|
PROXMOX_TOKEN_VALUE = os.getenv("PROXMOX_TOKEN_VALUE")
|
||||||
|
PROXMOX_VERIFY_SSL = os.getenv("PROXMOX_VERIFY_SSL", "False").lower() == "true"
|
||||||
|
PROXMOX_NODE = os.getenv("PROXMOX_NODE", "pve")
|
||||||
|
PROXMOX_VM_ID = os.getenv("PROXMOX_VM_ID", None)
|
||||||
|
PROXMOX_TYPE = os.getenv("PROXMOX_TYPE", "qemu")
|
||||||
|
|
||||||
DB_HOST = get_env_var("DB_HOST")
|
DB_HOST = get_env_var("DB_HOST")
|
||||||
DB_USER = get_env_var("DB_USER")
|
DB_USER = get_env_var("DB_USER")
|
||||||
DB_PASSWORD = get_env_var("DB_PASSWORD")
|
DB_PASSWORD = get_env_var("DB_PASSWORD")
|
||||||
@@ -54,9 +66,10 @@ DB_NAME = get_env_var("DB_NAME")
|
|||||||
|
|
||||||
LOCAL_TZ = pytz.timezone(get_env_var("LOCAL_TZ", str, required=False) or "America/Chicago")
|
LOCAL_TZ = pytz.timezone(get_env_var("LOCAL_TZ", str, required=False) or "America/Chicago")
|
||||||
|
|
||||||
BOT_VERSION = "1.0.6"
|
BOT_VERSION = "1.0.7"
|
||||||
VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt"
|
VERSION_URL = "https://raw.githubusercontent.com/PenguCCN/Jellycord/main/version.txt"
|
||||||
RELEASES_URL = "https://github.com/PenguCCN/Jellycord/releases"
|
RELEASES_URL = "https://github.com/PenguCCN/Jellycord/releases"
|
||||||
|
CHANGELOG_URL = "https://raw.githubusercontent.com/PenguCCN/Jellycord/refs/heads/main/CHANGELOG.md"
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# EVENT LOGGING
|
# EVENT LOGGING
|
||||||
@@ -81,15 +94,23 @@ bot = commands.Bot(command_prefix=PREFIX, intents=intents, help_command=None)
|
|||||||
# QBITTORRENT SETUP
|
# QBITTORRENT SETUP
|
||||||
# =====================
|
# =====================
|
||||||
|
|
||||||
|
if ENABLE_QBITTORRENT:
|
||||||
|
|
||||||
qb = qbittorrentapi.Client(
|
qb = qbittorrentapi.Client(
|
||||||
host=QBIT_HOST,
|
host=QBIT_HOST,
|
||||||
username=QBIT_USERNAME,
|
username=QBIT_USERNAME,
|
||||||
password=QBIT_PASSWORD
|
password=QBIT_PASSWORD
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
qb.auth_log_in()
|
qb.auth_log_in()
|
||||||
|
print("✅ Logged in to qBittorrent")
|
||||||
except qbittorrentapi.LoginFailed:
|
except qbittorrentapi.LoginFailed:
|
||||||
print("Failed to log in to qBittorrent API")
|
print("❌ Failed to log in to qBittorrent API")
|
||||||
|
qb = None
|
||||||
|
else:
|
||||||
|
qb = None # qBittorrent disabled
|
||||||
|
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# DATABASE SETUP
|
# DATABASE SETUP
|
||||||
@@ -282,6 +303,32 @@ def reset_jellyfin_password(username: str, new_password: str) -> bool:
|
|||||||
response = requests.post(f"{JELLYFIN_URL}/Users/{user_id}/Password", headers=headers, json=data)
|
response = requests.post(f"{JELLYFIN_URL}/Users/{user_id}/Password", headers=headers, json=data)
|
||||||
return response.status_code in (200, 204)
|
return response.status_code in (200, 204)
|
||||||
|
|
||||||
|
def create_trial_jellyfin_user(username, password):
|
||||||
|
payload = {
|
||||||
|
"Name": username,
|
||||||
|
"Password": password,
|
||||||
|
"Policy": {
|
||||||
|
"EnableDownloads": False,
|
||||||
|
"EnableSyncTranscoding": False,
|
||||||
|
"EnableRemoteControlOfOtherUsers": False,
|
||||||
|
"EnableLiveTvAccess": False,
|
||||||
|
"IsAdministrator": False,
|
||||||
|
"IsHidden": False,
|
||||||
|
"IsDisabled": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
"X-Emby-Token": JELLYFIN_API_KEY,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
response = requests.post(f"{JELLYFIN_URL}/Users/New", json=payload, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json().get("Id")
|
||||||
|
else:
|
||||||
|
print(f"[Jellyfin] Trial user creation failed. Status: {response.status_code}, Response: {response.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# JELLYSEERR HELPERS
|
# JELLYSEERR HELPERS
|
||||||
# =====================
|
# =====================
|
||||||
@@ -348,6 +395,109 @@ def progress_bar(progress: float, length: int = 20) -> str:
|
|||||||
bar = '█' * filled_length + '░' * (length - filled_length)
|
bar = '█' * filled_length + '░' * (length - filled_length)
|
||||||
return f"[{bar}] {progress*100:.2f}%"
|
return f"[{bar}] {progress*100:.2f}%"
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# PROXMOX HELPERS
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
def get_proxmox_client():
|
||||||
|
"""Create and return a Proxmox client using API token auth."""
|
||||||
|
if not (PROXMOX_HOST and PROXMOX_TOKEN_NAME and PROXMOX_TOKEN_VALUE):
|
||||||
|
raise ValueError("Proxmox API credentials are not fully configured in .env")
|
||||||
|
|
||||||
|
# Parse host + port safely
|
||||||
|
base_host = PROXMOX_HOST.replace("https://", "").replace("http://", "")
|
||||||
|
if ":" in base_host:
|
||||||
|
host, port = base_host.split(":")
|
||||||
|
else:
|
||||||
|
host, port = base_host, 8006 # default Proxmox port
|
||||||
|
|
||||||
|
# Split token into user + token name
|
||||||
|
try:
|
||||||
|
user, token_name = PROXMOX_TOKEN_NAME.split("!")
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
"❌ PROXMOX_TOKEN_NAME must be in the format 'user@realm!tokenid'"
|
||||||
|
)
|
||||||
|
|
||||||
|
log_event(f"[Proxmox] Connecting to {host}:{port} as {user} with token '{token_name}'")
|
||||||
|
|
||||||
|
return ProxmoxAPI(
|
||||||
|
host,
|
||||||
|
port=int(port),
|
||||||
|
user=user,
|
||||||
|
token_name=token_name,
|
||||||
|
token_value=PROXMOX_TOKEN_VALUE,
|
||||||
|
verify_ssl=PROXMOX_VERIFY_SSL
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# JFA-GO HELPERS
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
def refresh_jfa_token() -> bool:
|
||||||
|
"""
|
||||||
|
Authenticate to JFA-Go with username/password (Basic auth) against /token/login,
|
||||||
|
write the returned token to .env (JFA_TOKEN and JFA_API_KEY), and reload env.
|
||||||
|
Returns True on success.
|
||||||
|
"""
|
||||||
|
global JFA_TOKEN, JFA_API_KEY
|
||||||
|
|
||||||
|
if not (JFA_URL and JFA_USERNAME and JFA_PASSWORD):
|
||||||
|
print("[JFA] Missing JFA_URL/JFA_USERNAME/JFA_PASSWORD in environment.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
url = JFA_URL.rstrip("/") + "/token/login"
|
||||||
|
headers = {"accept": "application/json"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Option A: let requests build the Basic header
|
||||||
|
r = requests.get(url, auth=(JFA_USERNAME, JFA_PASSWORD), headers=headers, timeout=10)
|
||||||
|
|
||||||
|
# If you prefer to build the header manually (exactly like your curl), use:
|
||||||
|
# creds = f"{JFA_USERNAME}:{JFA_PASSWORD}".encode()
|
||||||
|
# b64 = base64.b64encode(creds).decode()
|
||||||
|
# headers["Authorization"] = f"Basic {b64}"
|
||||||
|
# r = requests.get(url, headers=headers, timeout=10)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
print(f"[JFA] token login failed: {r.status_code} - {r.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
data = r.json() if r.text else {}
|
||||||
|
# try common token fields
|
||||||
|
token = (
|
||||||
|
data.get("token")
|
||||||
|
or data.get("access_token")
|
||||||
|
or data.get("jwt")
|
||||||
|
or data.get("api_key")
|
||||||
|
or data.get("data") # sometimes nested
|
||||||
|
)
|
||||||
|
|
||||||
|
# If API returns {"token": "<token>"} -> good. If it returns a wrapped structure,
|
||||||
|
# try to handle a couple of other shapes:
|
||||||
|
if not token:
|
||||||
|
# if response is {'success': True} or {'invites':...} then no token present
|
||||||
|
# print for debugging
|
||||||
|
print("[JFA] token not found in response JSON:", data)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Persist token to .env under both names (compatibility)
|
||||||
|
_update_env_key("JFA_TOKEN", token)
|
||||||
|
_update_env_key("JFA_API_KEY", token)
|
||||||
|
|
||||||
|
# Update in-memory values and reload env
|
||||||
|
JFA_TOKEN = token
|
||||||
|
JFA_API_KEY = token
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
print("[JFA] Successfully refreshed token and updated .env")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[JFA] Exception while refreshing token: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# DISCORD HELPERS
|
# DISCORD HELPERS
|
||||||
# =====================
|
# =====================
|
||||||
@@ -403,31 +553,25 @@ 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):
|
def _update_env_key(key: str, value: str, env_path: str = ".env"):
|
||||||
payload = {
|
"""Update or append key=value in .env (keeps file order)."""
|
||||||
"Name": username,
|
lines = []
|
||||||
"Password": password,
|
found = False
|
||||||
"Policy": {
|
try:
|
||||||
"EnableDownloads": False,
|
with open(env_path, "r", encoding="utf-8") as f:
|
||||||
"EnableSyncTranscoding": False,
|
lines = f.readlines()
|
||||||
"EnableRemoteControlOfOtherUsers": False,
|
except FileNotFoundError:
|
||||||
"EnableLiveTvAccess": False,
|
lines = []
|
||||||
"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:
|
with open(env_path, "w", encoding="utf-8") as f:
|
||||||
return response.json().get("Id")
|
for line in lines:
|
||||||
|
if line.strip().startswith(f"{key}="):
|
||||||
|
f.write(f"{key}={value}\n")
|
||||||
|
found = True
|
||||||
else:
|
else:
|
||||||
print(f"[Jellyfin] Trial user creation failed. Status: {response.status_code}, Response: {response.text}")
|
f.write(line)
|
||||||
return None
|
if not found:
|
||||||
|
f.write(f"{key}={value}\n")
|
||||||
|
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
@@ -752,6 +896,27 @@ async def clearinvites(ctx):
|
|||||||
print(f"[clearinvites] Error: {e}", exc_info=True)
|
print(f"[clearinvites] Error: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def refreshjfakey(ctx):
|
||||||
|
"""Admin-only: Force refresh the JFA-Go API key using username/password auth."""
|
||||||
|
if not has_admin_role(ctx.author):
|
||||||
|
await ctx.send("❌ You don’t have permission to use this command.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not ENABLE_JFA:
|
||||||
|
await ctx.send("⚠️ JFA-Go integration is disabled in the configuration.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.send("🔁 Attempting to refresh JFA token...")
|
||||||
|
success = refresh_jfa_token()
|
||||||
|
if success:
|
||||||
|
await ctx.send("✅ Successfully refreshed the JFA-Go API token and updated `.env`")
|
||||||
|
log_event(f"Admin {ctx.author} forced a JFA API Token refresh")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to refresh the JFA-Go API token. Check bot logs for details.")
|
||||||
|
log_event(f"Admin {ctx.author} attempted JFA API Token refresh but failed")
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def trialaccount(ctx, username: str = None, password: str = None):
|
async def trialaccount(ctx, username: str = None, password: str = None):
|
||||||
"""Create a 24-hour trial Jellyfin account. DM-only, one-time per user."""
|
"""Create a 24-hour trial Jellyfin account. DM-only, one-time per user."""
|
||||||
@@ -888,8 +1053,9 @@ async def deleteaccount(ctx, username: str = None):
|
|||||||
await ctx.send(f"❌ Failed to delete Jellyfin account **{username}**.")
|
await ctx.send(f"❌ Failed to delete Jellyfin account **{username}**.")
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def what2watch(ctx):
|
async def movies2watch(ctx):
|
||||||
"""Pick 5 random movies from the Jellyfin library with embeds and posters."""
|
log_event(f"movies2watch invoked by {ctx.author}")
|
||||||
|
"""Pick 5 random movies from the Jellyfin library with embeds, and IMDb links."""
|
||||||
member = ctx.guild.get_member(ctx.author.id) if ctx.guild else None
|
member = ctx.guild.get_member(ctx.author.id) if ctx.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 to use this command.")
|
await ctx.send(f"❌ {ctx.author.mention}, you don’t have the required role to use this command.")
|
||||||
@@ -897,8 +1063,17 @@ async def what2watch(ctx):
|
|||||||
|
|
||||||
headers = {"X-Emby-Token": JELLYFIN_API_KEY}
|
headers = {"X-Emby-Token": JELLYFIN_API_KEY}
|
||||||
try:
|
try:
|
||||||
# Fetch all movies
|
# Fetch all movies (include ProviderIds explicitly!)
|
||||||
r = requests.get(f"{JELLYFIN_URL}/Items?IncludeItemTypes=Movie&Recursive=true", headers=headers, timeout=10)
|
r = requests.get(
|
||||||
|
f"{JELLYFIN_URL}/Items",
|
||||||
|
headers=headers,
|
||||||
|
params={
|
||||||
|
"IncludeItemTypes": "Movie",
|
||||||
|
"Recursive": "true",
|
||||||
|
"Fields": "ProviderIds"
|
||||||
|
},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
await ctx.send(f"❌ Failed to fetch movies. Status code: {r.status_code}")
|
await ctx.send(f"❌ Failed to fetch movies. Status code: {r.status_code}")
|
||||||
return
|
return
|
||||||
@@ -918,7 +1093,7 @@ async def what2watch(ctx):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for movie in selection:
|
for movie in selection:
|
||||||
name = movie.get("Name")
|
name = movie.get("Name", "Unknown Title")
|
||||||
year = movie.get("ProductionYear", "N/A")
|
year = movie.get("ProductionYear", "N/A")
|
||||||
runtime = movie.get("RunTimeTicks", None)
|
runtime = movie.get("RunTimeTicks", None)
|
||||||
runtime_min = int(runtime / 10_000_000 / 60) if runtime else "N/A"
|
runtime_min = int(runtime / 10_000_000 / 60) if runtime else "N/A"
|
||||||
@@ -928,11 +1103,16 @@ async def what2watch(ctx):
|
|||||||
if "PrimaryImageTag" in movie and movie["PrimaryImageTag"]:
|
if "PrimaryImageTag" in movie and movie["PrimaryImageTag"]:
|
||||||
poster_url = f"{JELLYFIN_URL}/Items/{movie['Id']}/Images/Primary?tag={movie['PrimaryImageTag']}&quality=90"
|
poster_url = f"{JELLYFIN_URL}/Items/{movie['Id']}/Images/Primary?tag={movie['PrimaryImageTag']}&quality=90"
|
||||||
|
|
||||||
field_value = f"Year: {year}\nRuntime: {runtime_min} min"
|
# IMDb link if available
|
||||||
|
imdb_id = movie.get("ProviderIds", {}).get("Imdb")
|
||||||
|
imdb_link = f"[IMDb Link](https://www.imdb.com/title/{imdb_id})" if imdb_id else "No IMDb ID available"
|
||||||
|
|
||||||
|
# Field content
|
||||||
|
field_value = f"Year: {year}\nRuntime: {runtime_min} min\n{imdb_link}"
|
||||||
embed.add_field(name=name, value=field_value, inline=False)
|
embed.add_field(name=name, value=field_value, inline=False)
|
||||||
|
|
||||||
if poster_url:
|
if poster_url:
|
||||||
embed.set_image(url=poster_url) # Only last movie's poster will appear as main embed image
|
embed.set_image(url=poster_url) # Only the last poster appears as main embed image
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
@@ -940,6 +1120,72 @@ async def what2watch(ctx):
|
|||||||
await ctx.send(f"❌ Error fetching movies: {e}")
|
await ctx.send(f"❌ Error fetching movies: {e}")
|
||||||
print(f"[what2watch] Error: {e}")
|
print(f"[what2watch] Error: {e}")
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def shows2watch(ctx):
|
||||||
|
log_event(f"shows2watch invoked by {ctx.author}")
|
||||||
|
"""Pick 5 random TV shows from the Jellyfin library with embeds, and IMDb links."""
|
||||||
|
member = ctx.guild.get_member(ctx.author.id) if ctx.guild else None
|
||||||
|
if not member or not has_required_role(member):
|
||||||
|
await ctx.send(f"❌ {ctx.author.mention}, you don’t have the required role to use this command.")
|
||||||
|
return
|
||||||
|
|
||||||
|
headers = {"X-Emby-Token": JELLYFIN_API_KEY}
|
||||||
|
try:
|
||||||
|
# Fetch all shows (include ProviderIds explicitly!)
|
||||||
|
r = requests.get(
|
||||||
|
f"{JELLYFIN_URL}/Items",
|
||||||
|
headers=headers,
|
||||||
|
params={
|
||||||
|
"IncludeItemTypes": "Series",
|
||||||
|
"Recursive": "true",
|
||||||
|
"Fields": "ProviderIds"
|
||||||
|
},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
if r.status_code != 200:
|
||||||
|
await ctx.send(f"❌ Failed to fetch shows. Status code: {r.status_code}")
|
||||||
|
return
|
||||||
|
|
||||||
|
shows = r.json().get("Items", [])
|
||||||
|
if not shows:
|
||||||
|
await ctx.send("⚠️ No shows found in the library.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pick 5 random shows
|
||||||
|
selection = random.sample(shows, min(5, len(shows)))
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="📺 Shows to Watch",
|
||||||
|
description="Here are 5 random TV show suggestions from the library:",
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
|
||||||
|
for show in selection:
|
||||||
|
name = show.get("Name", "Unknown Title")
|
||||||
|
year = show.get("ProductionYear", "N/A")
|
||||||
|
|
||||||
|
# Poster URL if available
|
||||||
|
poster_url = None
|
||||||
|
if "PrimaryImageTag" in show and show["PrimaryImageTag"]:
|
||||||
|
poster_url = f"{JELLYFIN_URL}/Items/{show['Id']}/Images/Primary?tag={show['PrimaryImageTag']}&quality=90"
|
||||||
|
|
||||||
|
# IMDb link if available
|
||||||
|
imdb_id = show.get("ProviderIds", {}).get("Imdb")
|
||||||
|
imdb_link = f"[IMDb Link](https://www.imdb.com/title/{imdb_id})" if imdb_id else "No IMDb ID available"
|
||||||
|
|
||||||
|
# Field content
|
||||||
|
field_value = f"Year: {year}\n{imdb_link}"
|
||||||
|
embed.add_field(name=name, value=field_value, inline=False)
|
||||||
|
|
||||||
|
if poster_url:
|
||||||
|
embed.set_image(url=poster_url) # Only the last show's poster will appear as the embed image
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.send(f"❌ Error fetching shows: {e}")
|
||||||
|
print(f"[shows2watch] Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def cleanup(ctx):
|
async def cleanup(ctx):
|
||||||
@@ -1225,6 +1471,130 @@ async def qbview(ctx):
|
|||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def metrics(ctx):
|
||||||
|
"""Check performance metrics for the configured Proxmox VM/Container."""
|
||||||
|
if not has_admin_role(ctx.author):
|
||||||
|
await ctx.send("❌ You don’t have permission to use this command.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not PROXMOX_VM_ID:
|
||||||
|
await ctx.send("⚠️ No Proxmox VM/Container ID is set in the .env file.")
|
||||||
|
return
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"PVEAPIToken={PROXMOX_TOKEN_NAME}={PROXMOX_TOKEN_VALUE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = f"{PROXMOX_HOST}/api2/json/nodes/{PROXMOX_NODE}/{PROXMOX_TYPE}/{PROXMOX_VM_ID}/status/current"
|
||||||
|
r = requests.get(url, headers=headers, verify=False, timeout=10)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
await ctx.send(f"❌ Failed to fetch VM/Container status (status {r.status_code})")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = r.json().get("data", {})
|
||||||
|
|
||||||
|
# Extract metrics
|
||||||
|
name = data.get("name", f"ID {PROXMOX_VM_ID}")
|
||||||
|
status = data.get("status", "unknown").capitalize()
|
||||||
|
cpu = round(data.get("cpu", 0) * 100, 2) # returns fraction, convert to %
|
||||||
|
maxmem = data.get("maxmem", 1)
|
||||||
|
mem = data.get("mem", 0)
|
||||||
|
mem_usage = round((mem / maxmem) * 100, 2) if maxmem > 0 else 0
|
||||||
|
maxdisk = data.get("maxdisk", 1)
|
||||||
|
disk = data.get("disk", 0)
|
||||||
|
disk_usage = round((disk / maxdisk) * 100, 2) if maxdisk > 0 else 0
|
||||||
|
maxswap = data.get("maxswap", 1)
|
||||||
|
swap = data.get("swap", 0)
|
||||||
|
swap_usage = round((swap / maxswap) * 100, 2) if maxswap > 0 else 0
|
||||||
|
uptime = data.get("uptime", 0)
|
||||||
|
|
||||||
|
# Build embed
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"📊 Proxmox Status: {name}",
|
||||||
|
color=discord.Color.green() if status == "Running" else discord.Color.red()
|
||||||
|
)
|
||||||
|
embed.add_field(name="Status", value=status, inline=True)
|
||||||
|
embed.add_field(name="CPU Usage", value=f"{cpu} %", inline=True)
|
||||||
|
embed.add_field(name="Memory Usage", value=f"{mem_usage} %", inline=True)
|
||||||
|
embed.add_field(name="Disk Usage", value=f"{disk_usage} %", inline=True)
|
||||||
|
embed.add_field(name="Swap Usage", value=f"{swap_usage} %", inline=True)
|
||||||
|
embed.add_field(name="Uptime", value=f"{uptime // 3600}h {(uptime % 3600) // 60}m", inline=True)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.send(f"❌ Error fetching Proxmox VM/Container status: {e}")
|
||||||
|
print(f"[proxmoxstatus] Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def storage(ctx):
|
||||||
|
"""Check Proxmox storage pools and ZFS pools."""
|
||||||
|
if not ENABLE_PROXMOX:
|
||||||
|
await ctx.send("⚠️ Proxmox integration is disabled in the configuration.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not has_admin_role(ctx.author):
|
||||||
|
await ctx.send("❌ You don’t have permission to use this command.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxmox = get_proxmox_client()
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="📦 Proxmox Storage",
|
||||||
|
description="Storage pool usage and ZFS pools",
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
|
||||||
|
for node in proxmox.nodes.get():
|
||||||
|
node_name = node["node"]
|
||||||
|
|
||||||
|
# ---- ZFS ----
|
||||||
|
try:
|
||||||
|
zfs_pools = proxmox.nodes(node_name).disks.zfs.get()
|
||||||
|
if zfs_pools:
|
||||||
|
zfs_info = [
|
||||||
|
f"**{p['name']}**: {p['alloc']/1024**3:.2f} GiB / "
|
||||||
|
f"{p['size']/1024**3:.2f} GiB ({(p['alloc']/p['size']*100):.1f}%)"
|
||||||
|
for p in zfs_pools
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
zfs_info = ["No ZFS pools found"]
|
||||||
|
except Exception as e:
|
||||||
|
zfs_info = [f"⚠️ Failed to fetch ZFS pools ({e})"]
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name=f"🖥️ {node_name} - ZFS Pools",
|
||||||
|
value="\n".join(zfs_info),
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- Normal storage (skip ZFS) ----
|
||||||
|
try:
|
||||||
|
storage_info = proxmox.nodes(node_name).storage.get()
|
||||||
|
normal_lines = [
|
||||||
|
f"**{s['storage']}**: {s['used']/1024**3:.2f} GiB / "
|
||||||
|
f"{s['total']/1024**3:.2f} GiB ({(s['used']/s['total']*100):.1f}%)"
|
||||||
|
for s in storage_info
|
||||||
|
if s.get("type", "").lower() not in ("zfspool", "zfs")
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
normal_lines = [f"⚠️ Failed to fetch normal storage ({e})"]
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name=f"🗳️ {node_name} - Normal Storage",
|
||||||
|
value="\n".join(normal_lines) or "No non‑ZFS storage found",
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.send(f"⚠️ Unexpected error: {e}")
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def link(ctx, jellyfin_username: str = None, user: discord.User = None, js_id: str = None):
|
async def link(ctx, jellyfin_username: str = None, user: discord.User = None, js_id: str = None):
|
||||||
log_event(f"link invoked by {ctx.author}")
|
log_event(f"link invoked by {ctx.author}")
|
||||||
@@ -1314,6 +1684,58 @@ async def updates(ctx):
|
|||||||
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 changelog(ctx):
|
||||||
|
log_event(f"changelog invoked by {ctx.author}")
|
||||||
|
"""Fetch and display the changelog for the current bot version."""
|
||||||
|
if not has_admin_role(ctx.author):
|
||||||
|
await ctx.send("❌ You don’t have permission to use this command.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(CHANGELOG_URL, timeout=10)
|
||||||
|
if r.status_code != 200:
|
||||||
|
await ctx.send(f"❌ Failed to fetch changelog (status {r.status_code})")
|
||||||
|
return
|
||||||
|
|
||||||
|
changelog_text = r.text
|
||||||
|
|
||||||
|
# Find the section for the current version
|
||||||
|
search_str = f"# {BOT_VERSION}"
|
||||||
|
start_idx = changelog_text.find(search_str)
|
||||||
|
if start_idx == -1:
|
||||||
|
await ctx.send(f"⚠️ No changelog found for version `{BOT_VERSION}`.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find the next heading or end of file
|
||||||
|
next_idx = changelog_text.find("# ", start_idx + len(search_str))
|
||||||
|
if next_idx == -1:
|
||||||
|
section = changelog_text[start_idx:].strip()
|
||||||
|
else:
|
||||||
|
section = changelog_text[start_idx:next_idx].strip()
|
||||||
|
|
||||||
|
# Clean the section (remove the "# version" line itself)
|
||||||
|
lines = section.splitlines()
|
||||||
|
if lines and lines[0].startswith("# "):
|
||||||
|
lines = lines[1:]
|
||||||
|
section_content = "\n".join(lines).strip()
|
||||||
|
|
||||||
|
if not section_content:
|
||||||
|
section_content = "⚠️ No details provided for this version."
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"📜 Changelog for v{BOT_VERSION}",
|
||||||
|
description=section_content,
|
||||||
|
color=discord.Color.purple()
|
||||||
|
)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.send(f"❌ Error fetching changelog: {e}")
|
||||||
|
print(f"[changelog] Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def logging(ctx, state: str):
|
async def logging(ctx, state: str):
|
||||||
"""Admin-only: Enable or disable event logging."""
|
"""Admin-only: Enable or disable event logging."""
|
||||||
@@ -1365,7 +1787,8 @@ async def help_command(ctx):
|
|||||||
f"`{PREFIX}createaccount <username> <password>` - Create your Jellyfin account",
|
f"`{PREFIX}createaccount <username> <password>` - Create your Jellyfin account",
|
||||||
f"`{PREFIX}recoveraccount <newpassword>` - Reset your password",
|
f"`{PREFIX}recoveraccount <newpassword>` - Reset your password",
|
||||||
f"`{PREFIX}deleteaccount <username>` - Delete your Jellyfin account",
|
f"`{PREFIX}deleteaccount <username>` - Delete your Jellyfin account",
|
||||||
f"`{PREFIX}what2watch` - Lists 5 random movie suggestions from the Jellyfin Library"
|
f"`{PREFIX}movies2watch` - Lists 5 random movie suggestions from the Jellyfin Library",
|
||||||
|
f"`{PREFIX}shows2watch` - Lists 5 random show suggestions from the Jellyfin Library"
|
||||||
]
|
]
|
||||||
if ENABLE_TRIAL_ACCOUNTS:
|
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 24-hour trial Jellyfin account")
|
||||||
@@ -1405,12 +1828,21 @@ async def help_command(ctx):
|
|||||||
]
|
]
|
||||||
embed.add_field(name="💾 qBittorrent Commands", value="\n".join(qb_cmds), inline=False)
|
embed.add_field(name="💾 qBittorrent Commands", value="\n".join(qb_cmds), inline=False)
|
||||||
|
|
||||||
|
# --- Proxmox Commands ---
|
||||||
|
if ENABLE_PROXMOX:
|
||||||
|
qb_cmds = [
|
||||||
|
f"`{PREFIX}storage` - Show available storage pools and free space",
|
||||||
|
f"`{PREFIX}metrics` - Show Jellyfin container metrics"
|
||||||
|
]
|
||||||
|
embed.add_field(name="🗳️ Proxmox Commands", value="\n".join(qb_cmds), inline=False)
|
||||||
|
|
||||||
# --- JFA Commands ---
|
# --- JFA Commands ---
|
||||||
if ENABLE_JFA:
|
if ENABLE_JFA:
|
||||||
jfa_cmds = [
|
jfa_cmds = [
|
||||||
f"`{PREFIX}createinvite` - Create a new JFA invite link",
|
f"`{PREFIX}createinvite` - Create a new JFA invite link",
|
||||||
f"`{PREFIX}listinvites` - List all active JFA invite links",
|
f"`{PREFIX}listinvites` - List all active JFA invite links",
|
||||||
f"`{PREFIX}deleteinvite <code>` - Delete a specific JFA invite"
|
f"`{PREFIX}deleteinvite <code>` - Delete a specific JFA invite",
|
||||||
|
f"`{PREFIX}refreshjfakey` - Refreshes the JFA API Key Forcefully"
|
||||||
]
|
]
|
||||||
embed.add_field(name="🔑 JFA Commands", value="\n".join(jfa_cmds), inline=False)
|
embed.add_field(name="🔑 JFA Commands", value="\n".join(jfa_cmds), inline=False)
|
||||||
|
|
||||||
@@ -1418,6 +1850,7 @@ async def help_command(ctx):
|
|||||||
admin_bot_cmds = [
|
admin_bot_cmds = [
|
||||||
f"`{PREFIX}setprefix` - Change the bot's command prefix",
|
f"`{PREFIX}setprefix` - Change the bot's command prefix",
|
||||||
f"`{PREFIX}updates` - Manually check for bot updates",
|
f"`{PREFIX}updates` - Manually check for bot updates",
|
||||||
|
f"`{PREFIX}changelog` - View changelog for current bot version",
|
||||||
f"`{PREFIX}logging` - Enable/disable console event logging"
|
f"`{PREFIX}logging` - Enable/disable console event logging"
|
||||||
]
|
]
|
||||||
embed.add_field(name="⚙️ Admin Bot Commands", value="\n".join(admin_bot_cmds), inline=False)
|
embed.add_field(name="⚙️ Admin Bot Commands", value="\n".join(admin_bot_cmds), inline=False)
|
||||||
@@ -1562,6 +1995,30 @@ async def cleanup_task():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Cleanup] Failed to send removed message to sync channel: {e}")
|
print(f"[Cleanup] Failed to send removed message to sync channel: {e}")
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# JFA-Go Scheduled Token Refresh
|
||||||
|
# =====================
|
||||||
|
if ENABLE_JFA:
|
||||||
|
|
||||||
|
@tasks.loop(hours=1)
|
||||||
|
async def refresh_jfa_loop():
|
||||||
|
success = refresh_jfa_token()
|
||||||
|
if success:
|
||||||
|
log_event("[JFA] Successfully refreshed token (scheduled loop).")
|
||||||
|
else:
|
||||||
|
log_event("[JFA] Failed to refresh token (scheduled loop).")
|
||||||
|
|
||||||
|
@refresh_jfa_loop.before_loop
|
||||||
|
async def before_refresh_jfa_loop():
|
||||||
|
await bot.wait_until_ready()
|
||||||
|
log_event("[JFA] Token refresh loop waiting until bot is ready.")
|
||||||
|
|
||||||
|
# Start the loop inside on_ready to ensure event loop exists
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
if not refresh_jfa_loop.is_running():
|
||||||
|
refresh_jfa_loop.start()
|
||||||
|
log_event(f"Bot is ready. Logged in as {bot.user}")
|
||||||
|
|
||||||
@tasks.loop(hours=1)
|
@tasks.loop(hours=1)
|
||||||
async def check_for_updates():
|
async def check_for_updates():
|
||||||
@@ -1575,7 +2032,7 @@ async def check_for_updates():
|
|||||||
await log_channel.send(
|
await log_channel.send(
|
||||||
f"📌 Current version: `{BOT_VERSION}`\n"
|
f"📌 Current version: `{BOT_VERSION}`\n"
|
||||||
f"⬆️ Latest version: `{latest_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}"
|
f"{RELEASES_URL}"
|
||||||
)
|
)
|
||||||
log_event(f"Latest Version:'{latest_version}', Current Version: '{BOT_VERSION}'")
|
log_event(f"Latest Version:'{latest_version}', Current Version: '{BOT_VERSION}'")
|
||||||
@@ -1607,6 +2064,10 @@ async def on_ready():
|
|||||||
if not cleanup_task.is_running():
|
if not cleanup_task.is_running():
|
||||||
cleanup_task.start()
|
cleanup_task.start()
|
||||||
|
|
||||||
|
if ENABLE_JFA:
|
||||||
|
if not refresh_jfa_loop.is_running():
|
||||||
|
refresh_jfa_loop.start()
|
||||||
|
|
||||||
if not check_for_updates.is_running():
|
if not check_for_updates.is_running():
|
||||||
check_for_updates.start()
|
check_for_updates.start()
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ python-dotenv==1.0.1
|
|||||||
pytz==2025.2
|
pytz==2025.2
|
||||||
apscheduler==3.11.0
|
apscheduler==3.11.0
|
||||||
qbittorrent-api==2025.7.0
|
qbittorrent-api==2025.7.0
|
||||||
|
proxmoxer==2.2.0
|
||||||
@@ -1 +1 @@
|
|||||||
{ "version": "1.0.6" }
|
{ "version": "1.0.8" }
|
||||||
@@ -1 +1 @@
|
|||||||
1.0.6
|
1.0.8
|
||||||
Reference in New Issue
Block a user