6 Commits
1.0.9 ... main

Author SHA1 Message Date
f11e1997fd Partial File Logging 2025-12-12 03:36:43 -06:00
54099b85c1 Push Stats command 2025-12-12 03:21:41 -06:00
e4757805ae Push Basic Servarr Support 2025-12-12 02:30:14 -06:00
97dc1dcb98 Fix sending POST when feature disabled 2025-12-11 03:01:12 -06:00
61dc5db6d0 Added Statistic Tracking 2025-12-11 02:46:57 -06:00
Pengu
fdc6df373d Update ROADMAP.md 2025-10-26 11:32:50 -05:00
6 changed files with 466 additions and 15 deletions

View File

@@ -1,24 +1,24 @@
#Jellycord version 1.0.9 # Jellycord version 1.1.0
# Discord # |Discord|
DISCORD_TOKEN=your_discord_bot_token DISCORD_TOKEN=your_discord_bot_token
PREFIX=! PREFIX=!
GUILD_IDS=123456789012345678,123456789012345678 GUILD_IDS=123456789012345678,123456789012345678
ADMIN_ROLE_IDS=111111111111111111,222222222222222222 ADMIN_ROLE_IDS=111111111111111111,222222222222222222
REQUIRED_ROLE_IDS=333333333333333333,444444444444444444 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 ENABLE_TRIAL_ACCOUNTS=false
TRIAL_TIME=24 # In hours TRIAL_TIME=24 # In hours
# Jellyseerr # |Jellyseerr|
JELLYSEERR_ENABLED=false 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
# JFA-Go # |JFA-Go|
ENABLE_JFA=false ENABLE_JFA=false
JFA_URL=http://localhost:8056 JFA_URL=http://localhost:8056
JFA_USERNAME=yourusername JFA_USERNAME=yourusername
@@ -26,13 +26,22 @@ JFA_PASSWORD=yourpassword
JFA_API_KEY=your_api_key_here JFA_API_KEY=your_api_key_here
JFA_TOKEN= JFA_TOKEN=
# QBittorrent # |Servarr|
RADARR_URL=http://your-radarr-ip:7878
RADARR_API_KEY=yourradarrapikey
ENABLE_RADARR=false
SONARR_URL=http://your-sonarr-ip:8989
SONARR_API_KEY=yoursonarrapikey
ENABLE_SONARR=false
# |QBittorrent|
ENABLE_QBITTORRENT=false ENABLE_QBITTORRENT=false
QBIT_HOST=http://localhost:8080 QBIT_HOST=http://localhost:8080
QBIT_USERNAME=your_username QBIT_USERNAME=your_username
QBIT_PASSWORD=your_password QBIT_PASSWORD=your_password
# Proxmox # |Proxmox|
ENABLE_PROXMOX=false ENABLE_PROXMOX=false
PROXMOX_HOST=https://your-proxmox-server:8006 PROXMOX_HOST=https://your-proxmox-server:8006
PROXMOX_TOKEN_NAME=root@pam!yourtokenname PROXMOX_TOKEN_NAME=root@pam!yourtokenname
@@ -43,15 +52,17 @@ PROXMOX_VM_ID=
# Proxmox type can be "qemu" for VM, "lxc" for container # Proxmox type can be "qemu" for VM, "lxc" for container
PROXMOX_TYPE= PROXMOX_TYPE=
# MySQL # |MySQL|
DB_HOST=localhost DB_HOST=localhost
DB_USER=root DB_USER=root
DB_PASSWORD=password DB_PASSWORD=password
DB_NAME=jellyfin_bot DB_NAME=jellyfin_bot
# Time Settings # |General Settings|
TIMEZONE=America/Chicago TIMEZONE=America/Chicago
# Tracking only reports limited information about your instance for development reasons. (Tracking enabled Instance & Enabled Features)
TRACKING_ENABLED=true
# Logs # |Logs|
SYNC_LOG_CHANNEL_ID=555555555555555555 SYNC_LOG_CHANNEL_ID=555555555555555555
EVENT_LOGGING=false EVENT_LOGGING=false

View File

@@ -1,3 +1,8 @@
# 1.1.0
- The bot now sends limited info stats to a public frontend for development insight. (Reports enabled features and bot instance only | Jellyseerr, Proxmox, JFA, qBittorrent)
- Radarr & Sonarr Support
# 1.0.9 # 1.0.9
- You can now configure trial account duration - You can now configure trial account duration

View File

@@ -1,6 +1,13 @@
# Jellycord # Jellycord
![image](https://cdn.pengucc.com/images/projects/jellycord/readme/BannerRound.png) ![image](https://cdn.pengucc.com/images/projects/jellycord/readme/BannerRound.png)
![Live Player Count](https://img.shields.io/badge/dynamic/json?query=$.data.result[0].value[1]&url=https%3A%2F%2Fprometheus.pengucc.com%2Fapi%2Fv1%2Fquery%3Fquery%3Dinstance&style=for-the-badge&logo=nodedotjs&logoColor=white&label=Bot%20Instances&color=9964c5)
![Live Player Count](https://img.shields.io/badge/dynamic/json?query=$.data.result[0].value[1]&url=https%3A%2F%2Fprometheus.pengucc.com%2Fapi%2Fv1%2Fquery%3Fquery%3Djellyseerr&style=for-the-badge&logo=jellyfin&logoColor=white&label=Jellyseer%20Enabled&color=9964c5)
![Live Player Count](https://img.shields.io/badge/dynamic/json?query=$.data.result[0].value[1]&url=https%3A%2F%2Fprometheus.pengucc.com%2Fapi%2Fv1%2Fquery%3Fquery%3Dproxmox&style=for-the-badge&logo=proxmox&logoColor=white&label=Proxmox%20Enabled&color=9964c5)
![Live Player Count](https://img.shields.io/badge/dynamic/json?query=$.data.result[0].value[1]&url=https%3A%2F%2Fprometheus.pengucc.com%2Fapi%2Fv1%2Fquery%3Fquery%3Djfa&style=for-the-badge&logo=go&logoColor=white&label=JFA-GO%20Enabled&color=9964c5)
![Live Player Count](https://img.shields.io/badge/dynamic/json?query=$.data.result[0].value[1]&url=https%3A%2F%2Fprometheus.pengucc.com%2Fapi%2Fv1%2Fquery%3Fquery%3Dqbittorrent&style=for-the-badge&logo=qbittorrent&logoColor=white&label=qBittorrent%20Enabled&color=9964c5)
![Live Player Count](https://img.shields.io/badge/dynamic/json?query=$.data.result[0].value[1]&url=https%3A%2F%2Fprometheus.pengucc.com%2Fapi%2Fv1%2Fquery%3Fquery%3Dradarr&style=for-the-badge&logo=radarr&logoColor=white&label=Radarr%20Enabled&color=9964c5)
![Live Player Count](https://img.shields.io/badge/dynamic/json?query=$.data.result[0].value[1]&url=https%3A%2F%2Fprometheus.pengucc.com%2Fapi%2Fv1%2Fquery%3Fquery%3Dsonarr&style=for-the-badge&logo=sonarr&logoColor=white&label=Sonarr%20Enabled&color=9964c5)
[![Online Members](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscordapp.com%2Fapi%2Finvites%2FEdPJAhrDq8%3Fwith_counts%3Dtrue&query=approximate_presence_count&style=for-the-badge&logo=discord&logoColor=white&label=ONLINE%20MEMBERS&labelColor=grey&color=239eda)](https://discord.gg/EdPJAhrDq8) [![Online Members](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscordapp.com%2Fapi%2Finvites%2FEdPJAhrDq8%3Fwith_counts%3Dtrue&query=approximate_presence_count&style=for-the-badge&logo=discord&logoColor=white&label=ONLINE%20MEMBERS&labelColor=grey&color=239eda)](https://discord.gg/EdPJAhrDq8)
![Latest Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FPenguCCN%2FJellycord%2Fmain%2Fversion.json&query=%24.version&style=for-the-badge&logo=python&logoColor=white&label=Latest%20Version%3A&color=239eda) ![Latest Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FPenguCCN%2FJellycord%2Fmain%2Fversion.json&query=%24.version&style=for-the-badge&logo=python&logoColor=white&label=Latest%20Version%3A&color=239eda)
@@ -13,7 +20,24 @@ 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 # 🛑 Disclaimer (Please Read) 🛑
This bot collects limited amounts of information, it is enabled by default and can be disabled in your environment file. Information Collected is limited to:
- Bot Instances
- Enabled Features
We do **not** and will **never** collect nor store:
- IP Address's.
- System Information.
- Information about Jellyfin or any related services.
## Why do we do this?
Adding these simply allows me to see what features users are interested in, allowing me to focus on improving/fixing highly used features.
# Features
- Automatic Account Cleanup - Automatic Account Cleanup
- Creating Accounts - Creating Accounts
@@ -24,7 +48,7 @@ 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.**
@@ -85,6 +109,7 @@ 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
- `!stats` - View Local and Global Jellycord Stats
- `!update` - Download latest bot version - `!update` - Download latest bot version
- `!backup` - Create a backup of the bot and configurations - `!backup` - Create a backup of the bot and configurations
- `!backups` - List backups of the bot - `!backups` - List backups of the bot

View File

@@ -1,6 +1,7 @@
- **Future Features** - **Future Features**
- Servarr Support - Servarr Support
- Make Jellyfin admins exempt from account cleanup
- **To Do** - **To Do**

410
app.py
View File

@@ -18,6 +18,10 @@ from pathlib import Path
import tempfile import tempfile
import shutil import shutil
import pymysql import pymysql
import json
import psutil
import platform
import logging
# ===================== # =====================
# ENV + VALIDATION # ENV + VALIDATION
@@ -55,6 +59,14 @@ JFA_USERNAME = os.getenv("JFA_USERNAME")
JFA_PASSWORD = os.getenv("JFA_PASSWORD") JFA_PASSWORD = os.getenv("JFA_PASSWORD")
JFA_API_KEY = os.getenv("JFA_API_KEY") JFA_API_KEY = os.getenv("JFA_API_KEY")
ENABLE_RADARR = os.getenv("ENABLE_RADARR", "false").lower() == "true"
RADARR_URL = os.getenv("RADARR_URL", "").rstrip("/")
RADARR_API_KEY = os.getenv("RADARR_API_KEY", "")
ENABLE_SONARR = os.getenv("ENABLE_SONARR", "false").lower() == "true"
SONARR_URL = os.getenv("SONARR_URL", "").rstrip("/")
SONARR_API_KEY = os.getenv("SONARR_API_KEY", "")
ENABLE_QBITTORRENT = os.getenv("ENABLE_QBITTORRENT", "False").lower() == "true" ENABLE_QBITTORRENT = os.getenv("ENABLE_QBITTORRENT", "False").lower() == "true"
QBIT_HOST = os.getenv("QBIT_HOST") QBIT_HOST = os.getenv("QBIT_HOST")
QBIT_USERNAME = os.getenv("QBIT_USERNAME") QBIT_USERNAME = os.getenv("QBIT_USERNAME")
@@ -78,12 +90,29 @@ LOCAL_TZ = pytz.timezone(get_env_var("LOCAL_TZ", str, required=False) or "Americ
ENV_FILE = ".env" ENV_FILE = ".env"
DEFAULT_ENV_FILE = ".env.example" DEFAULT_ENV_FILE = ".env.example"
BACKUP_DIR = Path("backups") BACKUP_DIR = Path("backups")
LOG_DIR = Path("logs")
LOG_DIR.mkdir(exist_ok=True)
LATEST_LOG = LOG_DIR / "latest.log"
ARCHIVE_DIR = LOG_DIR / "archives"
ARCHIVE_DIR.mkdir(exist_ok=True)
BOT_VERSION = "1.0.9" BOT_VERSION = "1.0.9"
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" CHANGELOG_URL = "https://raw.githubusercontent.com/PenguCCN/Jellycord/refs/heads/main/CHANGELOG.md"
TRACKING_ENABLED = os.getenv("TRACKING_ENABLED", "False").lower() == "true"
PROMETHEUS_URL = "https://prometheus.pengucc.com/api/v1/query"
POST_ENDPOINTS = {
"botinstance": "https://jellycordstats.pengucc.com/api/instance",
"jellyseerr": "https://jellycordstats.pengucc.com/api/jellyseerr",
"proxmox": "https://jellycordstats.pengucc.com/api/proxmox",
"jfa": "https://jellycordstats.pengucc.com/api/jfa",
"qbittorrent": "https://jellycordstats.pengucc.com/api/qbittorrent",
"radarr": "https://jellycordstats.pengucc.com/api/radarr",
"sonarr": "https://jellycordstats.pengucc.com/api/sonarr"
}
# ===================== # =====================
# EVENT LOGGING # EVENT LOGGING
# ===================== # =====================
@@ -231,6 +260,14 @@ def init_trial_accounts_table():
cur.close() cur.close()
conn.close() conn.close()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LATEST_LOG, encoding="utf-8"),
logging.StreamHandler()
]
)
def get_accounts(): def get_accounts():
conn = mysql.connector.connect( conn = mysql.connector.connect(
@@ -284,6 +321,7 @@ def delete_account(discord_id):
# ===================== # =====================
# JELLYFIN HELPERS # JELLYFIN HELPERS
# ===================== # =====================
def create_jellyfin_user(username, password): def create_jellyfin_user(username, password):
headers = {"X-Emby-Token": JELLYFIN_API_KEY} headers = {"X-Emby-Token": JELLYFIN_API_KEY}
data = {"Name": username, "Password": password} data = {"Name": username, "Password": password}
@@ -398,6 +436,81 @@ def delete_jellyseerr_user(js_id: str) -> bool:
print(f"[Jellyseerr] Failed to delete user {js_id}: {e}") print(f"[Jellyseerr] Failed to delete user {js_id}: {e}")
return False return False
# =====================
# SERVARR HELPERS
# =====================
def radarr_get_movies():
"""Return a list of all movies Radarr is managing."""
if not ENABLE_RADARR:
return None
try:
response = requests.get(
f"{RADARR_URL}/api/v3/movie",
headers={"X-Api-Key": RADARR_API_KEY},
timeout=10
)
if response.status_code != 200:
print(f"[Radarr] Error fetching movies: {response.status_code} {response.text}")
return None
return response.json()
except Exception as e:
print(f"[Radarr] Exception: {e}")
return None
def radarr_get_latest_movies(count=5):
"""Return the latest added movies from Radarr."""
movies = radarr_get_movies()
if not movies:
return None
# Sort by 'added' field if available
sorted_movies = sorted(
movies,
key=lambda m: m.get("added", ""),
reverse=True
)
return sorted_movies[:count]
def sonarr_get_series():
"""Return a list of all series Sonarr is managing."""
if not ENABLE_SONARR:
return None
try:
response = requests.get(
f"{SONARR_URL}/api/v3/series",
headers={"X-Api-Key": SONARR_API_KEY},
timeout=10
)
if response.status_code != 200:
print(f"[Sonarr] Error fetching series: {response.status_code} {response.text}")
return None
return response.json()
except Exception as e:
print(f"[Sonarr] Exception: {e}")
return None
def sonarr_get_latest_series(count=5):
"""Return the latest added series from Sonarr."""
series = sonarr_get_series()
if not series:
return None
# Sonarr tracks `added` timestamps too
sorted_series = sorted(
series,
key=lambda s: s.get("added", ""),
reverse=True
)
return sorted_series[:count]
# ===================== # =====================
# QBITTORRENT HELPERS # QBITTORRENT HELPERS
# ===================== # =====================
@@ -671,6 +784,24 @@ def restart_bot():
"""Replace current process with a new one.""" """Replace current process with a new one."""
os.execv(sys.executable, [sys.executable] + sys.argv) os.execv(sys.executable, [sys.executable] + sys.argv)
def build_payload(enabled: bool):
return {"value": 1 if enabled else 0}
def promql(query: str):
"""Run a PromQL query and return results."""
try:
response = requests.get(
PROMETHEUS_URL,
params={"query": query},
timeout=10
)
data = response.json()
result = data.get("data", {}).get("result", [])
return result
except Exception as e:
print(f"[Prometheus] Error: {e}")
return None
# ===================== # =====================
# EVENTS # EVENTS
@@ -1513,6 +1644,100 @@ async def activestreams(ctx):
await ctx.send(f"❌ Error fetching active streams: {e}") await ctx.send(f"❌ Error fetching active streams: {e}")
print(f"[activestreams] Error: {e}") print(f"[activestreams] Error: {e}")
@bot.command()
async def moviestats(ctx):
"""Show Radarr's latest 5 added movies with total count."""
if not ENABLE_RADARR:
await ctx.send("⚠️ Radarr support is not enabled.")
return
movies = radarr_get_movies()
if movies is None:
await ctx.send("❌ Failed to connect to Radarr.")
return
total_count = len(movies)
# Sort by newest "added"
latest = sorted(
movies,
key=lambda m: m.get("added", ""),
reverse=True
)[:5]
embed = discord.Embed(
title="🎞️ Latest Radarr Additions",
color=discord.Color.orange()
)
for movie in latest:
title = movie.get("title", "Unknown")
year = movie.get("year", "Unknown")
added = movie.get("added", "Unknown")
tmdb_id = movie.get("tmdbId")
tmdb_link = (
f"https://www.themoviedb.org/movie/{tmdb_id}"
if tmdb_id else "No TMDB ID"
)
embed.add_field(
name=f"{title} ({year})",
value=f"📅 Added: `{added}`\n🔗 {tmdb_link}",
inline=False
)
embed.set_footer(text=f"Total movies managed by Radarr: {total_count}")
await ctx.send(embed=embed)
@bot.command()
async def showstats(ctx):
"""Show Sonarr's latest 5 added series with total count."""
if not ENABLE_SONARR:
await ctx.send("⚠️ Sonarr support is not enabled.")
return
series = sonarr_get_series()
if series is None:
await ctx.send("❌ Failed to connect to Sonarr.")
return
total_count = len(series)
# Newest first
latest = sorted(
series,
key=lambda s: s.get("added", ""),
reverse=True
)[:5]
embed = discord.Embed(
title="📺 Latest Sonarr Additions",
color=discord.Color.blue()
)
for show in latest:
title = show.get("title", "Unknown")
year = show.get("year", "Unknown")
added = show.get("added", "Unknown")
tvdb_id = show.get("tvdbId")
tvdb_link = (
f"https://thetvdb.com/?id={tvdb_id}&tab=series"
if tvdb_id else "No TVDB ID"
)
embed.add_field(
name=f"{title} ({year})",
value=f"📅 Added: `{added}`\n🔗 {tvdb_link}",
inline=False
)
embed.set_footer(text=f"Total series managed by Sonarr: {total_count}")
await ctx.send(embed=embed)
@bot.command() @bot.command()
async def qbview(ctx): async def qbview(ctx):
"""Admin-only: View current qBittorrent downloads.""" """Admin-only: View current qBittorrent downloads."""
@@ -1732,6 +1957,103 @@ async def unlink(ctx, discord_user: discord.User = None):
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()
async def stats(ctx):
"""Show unified system and Prometheus metrics in one compact embed."""
# -------------------
# Local System Stats
# -------------------
cpu_usage = psutil.cpu_percent(interval=1)
mem = psutil.virtual_memory()
mem_str = f"{round(mem.used/1024**3,2)} / {round(mem.total/1024**3,2)} GB ({mem.percent}%)"
disk = psutil.disk_usage('/')
disk_str = f"{round(disk.used/1024**3,2)} / {round(disk.total/1024**3,2)} GB ({disk.percent}%)"
boot_time = datetime.datetime.fromtimestamp(psutil.boot_time())
uptime = datetime.datetime.now() - boot_time
uptime_str = str(uptime).split('.')[0]
python_version = platform.python_version()
bot_ver = BOT_VERSION if "BOT_VERSION" in globals() else "Unknown"
# -------------------
# Prometheus Stats (Last 5 Minutes)
# -------------------
prometheus_fields = []
if PROMETHEUS_URL:
metrics = {
"Instances":
"max_over_time(instance[5m])",
"Jellyseerr Enabled":
"max_over_time(jellyseerr[5m])",
"JFA Enabled":
"max_over_time(jfa[5m])",
"qBittorrent Enabled":
"max_over_time(qbittorrent[5m])",
"Radarr Enabled":
"max_over_time(radarr[5m])",
"Sonarr Enabled":
"max_over_time(sonarr[5m])"
}
for label, query in metrics.items():
result = promql(query)
if result and isinstance(result, list) and len(result) > 0:
try:
value = result[0]["value"][1]
except:
value = "N/A"
else:
value = "N/A"
prometheus_fields.append((label, value))
# -------------------
# Build Embed
# -------------------
embed = discord.Embed(
title="📊 Jellycord System & Tracking Statistics",
color=discord.Color.blurple()
)
# Local stats
embed.add_field(name="🧠 CPU", value=f"{cpu_usage}%", inline=True)
embed.add_field(name="💾 Memory", value=mem_str, inline=True)
embed.add_field(name="📀 Disk", value=disk_str, inline=True)
embed.add_field(name="⏱️ Uptime", value=uptime_str, inline=True)
embed.add_field(name="🐍 Python", value=python_version, inline=True)
embed.add_field(name="🤖 Bot Version", value=bot_ver, inline=True)
# Prometheus stats
if prometheus_fields:
embed.add_field(name="📡 Tracking (Last 5 Minutes)", value="\u200b", inline=False)
for label, val in prometheus_fields:
embed.add_field(name=f"{label}", value=f"`{val}`", inline=True)
else:
embed.add_field(
name="📡 Tracking",
value="Prometheus disabled or unreachable",
inline=False
)
embed.set_footer(text=f"Generated • {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
await ctx.send(embed=embed)
@bot.command() @bot.command()
async def setprefix(ctx, new_prefix: str = None): async def setprefix(ctx, new_prefix: str = None):
@@ -2126,7 +2448,9 @@ async def help_command(ctx):
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}movies2watch` - 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" f"`{PREFIX}shows2watch` - Lists 5 random show suggestions from the Jellyfin Library",
f"`{PREFIX}moviestats` - Lists latest 5 movies added, also shows total movie library size",
f"`{PREFIX}showstats` - Lists latest 5 movies added, also shows total series library size"
] ]
if ENABLE_TRIAL_ACCOUNTS: if ENABLE_TRIAL_ACCOUNTS:
user_cmds.append(f"`{PREFIX}trialaccount <username> <password>` - Create a {TRIAL_TIME}-hour trial Jellyfin account") user_cmds.append(f"`{PREFIX}trialaccount <username> <password>` - Create a {TRIAL_TIME}-hour trial Jellyfin account")
@@ -2186,6 +2510,7 @@ async def help_command(ctx):
# Admin Bot commands # Admin Bot commands
admin_bot_cmds = [ admin_bot_cmds = [
f"`{PREFIX}stats` - View Local and Global Jellycord Stats",
f"`{PREFIX}setprefix` - Change the bot's command prefix", f"`{PREFIX}setprefix` - Change the bot's command prefix",
f"`{PREFIX}update` - Download latest bot version", f"`{PREFIX}update` - Download latest bot version",
f"`{PREFIX}backup` - Create a backup of the bot, its database and configurations", f"`{PREFIX}backup` - Create a backup of the bot, its database and configurations",
@@ -2337,6 +2662,83 @@ 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}")
@tasks.loop(hours=24)
async def rotate_logs():
try:
now = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# =====================
# 1. Ensure latest.log exists
# =====================
if not LATEST_LOG.exists():
LATEST_LOG.touch()
# =====================
# 2. Archive the latest.log
# =====================
archive_name = ARCHIVE_DIR / f"log_{now}.zip"
with zipfile.ZipFile(archive_name, "w", zipfile.ZIP_DEFLATED) as zipf:
zipf.write(LATEST_LOG, arcname="latest.log")
print(f"[LOG ROTATE] Archived log to: {archive_name}")
# =====================
# 3. Delete oldest archives if more than 4 exist
# =====================
archives = sorted(ARCHIVE_DIR.glob("*.zip"), key=os.path.getmtime)
if len(archives) > 4:
to_delete = archives[: len(archives) - 4]
for old in to_delete:
old.unlink()
print(f"[LOG ROTATE] Deleted old archive: {old}")
# =====================
# 4. Clear latest.log for new logs
# =====================
with open(LATEST_LOG, "w", encoding="utf-8"):
pass
print("[LOG ROTATE] Reset latest.log")
except Exception as e:
print(f"[ERROR] Log rotation failed: {e}")
@tasks.loop(seconds=15)
async def periodic_post_task():
if not TRACKING_ENABLED:
return
features = {
"botinstance": TRACKING_ENABLED,
"jellyseerr": JELLYSEERR_ENABLED,
"proxmox": ENABLE_PROXMOX,
"jfa": ENABLE_JFA,
"qbittorrent": ENABLE_QBITTORRENT,
"radarr": ENABLE_RADARR,
"sonarr": ENABLE_SONARR
}
for feature, enabled in features.items():
url = POST_ENDPOINTS.get(feature)
if not url:
print(f"[POST LOOP] No endpoint for: {feature}")
continue
# Skip POST if the feature is disabled (0)
if not enabled:
print(f"[POST LOOP] Skipping {feature} because it's disabled.")
continue
payload = build_payload(enabled)
try:
response = requests.post(url, json=payload, timeout=10)
print(f"[POST LOOP] Sent {feature}{response.status_code} | Payload: {payload}")
except Exception as e:
print(f"[POST LOOP] Error sending POST for {feature}: {e}")
# ===================== # =====================
# JFA-Go Scheduled Token Refresh # JFA-Go Scheduled Token Refresh
# ===================== # =====================
@@ -2413,6 +2815,12 @@ async def on_ready():
if not check_for_updates.is_running(): if not check_for_updates.is_running():
check_for_updates.start() check_for_updates.start()
if TRACKING_ENABLED:
print("Tracking enabled — starting.")
periodic_post_task.start()
else:
print("Tracking disabled via .env")
await bot.change_presence( await bot.change_presence(
activity=discord.Activity(type=discord.ActivityType.watching, name=f"{PREFIX}help") activity=discord.Activity(type=discord.ActivityType.watching, name=f"{PREFIX}help")
) )

View File

@@ -7,3 +7,4 @@ apscheduler==3.11.0
qbittorrent-api==2025.7.0 qbittorrent-api==2025.7.0
proxmoxer==2.2.0 proxmoxer==2.2.0
pymysql==1.1.2 pymysql==1.1.2
psutil==7.1.3