From 0d605cdd9225b28528e2b4fac05d89ba5f48c47a Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:11:19 +0000 Subject: [PATCH 01/16] Add Torrent Tracker Signup Bot - Created torrent_tracker_bot.py with comprehensive tracker monitoring - Monitors 6 popular private trackers (RED, OPS, PTP, BTN, HDB, AB) - Features user subscription system with Discord commands - Real-time notifications when tracker signups open - Persistent storage of subscriptions and tracker status - Added requirements.txt for dependencies - Created detailed README_torrent_tracker_bot.md documentation - Updated main README.md to document both bots The bot includes commands for: - !trackers - List all monitored trackers and status - !subscribe/!unsubscribe - Manage notifications - !subscriptions - View user's subscriptions - !status - Bot status and statistics - !help - Command help Bot checks tracker signup pages every 5 minutes and notifies subscribers when signups become available. Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- README.md | 70 ++++++ README_torrent_tracker_bot.md | 154 +++++++++++++ requirements.txt | 23 ++ torrent_tracker_bot.py | 398 ++++++++++++++++++++++++++++++++++ 4 files changed, 645 insertions(+) create mode 100644 README.md create mode 100644 README_torrent_tracker_bot.md create mode 100644 requirements.txt create mode 100644 torrent_tracker_bot.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..a52895e --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Discord Bots Collection + +This repository contains multiple Discord bots for different purposes. + +## Bots Available + +### 1. Nextcloud Support Bot (`bot.py`) +A Discord bot that provides automated support for Nextcloud-related questions. + +**Features:** +- Responds to common Nextcloud questions +- Provides documentation links for migration, reverse proxy setup, and OCC commands +- Interactive reverse proxy configuration help +- Supports Nginx, Apache, and Caddy configurations + +**Usage:** +- Configure the `TARGET_CHANNEL_ID` and bot token +- Run with: `python bot.py` + +### 2. Torrent Tracker Signup Bot (`torrent_tracker_bot.py`) +A Discord bot that monitors popular private torrent trackers for open signups and notifies users. + +**Features:** +- Monitors 6 popular private trackers (RED, OPS, PTP, BTN, HDB, AB) +- User subscription system +- Real-time notifications when signups open +- Persistent storage of subscriptions and tracker status +- Comprehensive command system + +**Usage:** +- See `README_torrent_tracker_bot.md` for detailed setup instructions +- Install dependencies: `pip install -r requirements.txt` +- Configure bot token and channel ID +- Run with: `python torrent_tracker_bot.py` + +## Configuration Files + +The repository also includes example reverse proxy configurations: +- `nextcloud_nginx.conf` - Nginx configuration for Nextcloud +- `apache.conf` - Apache configuration for Nextcloud +- `Caddyfile` - Caddy configuration for Nextcloud + +## Setup Requirements + +### For Nextcloud Bot: +- Python 3.6+ +- discord.py library +- Discord bot token + +### For Torrent Tracker Bot: +- Python 3.8+ +- Dependencies from `requirements.txt` +- Discord bot token +- Target channel ID configuration + +## License + +This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details. + +## Contributing + +Feel free to contribute by: +- Adding new bot features +- Improving existing functionality +- Adding support for more services +- Reporting bugs and issues + +## Disclaimer + +These bots are for educational and informational purposes. Users are responsible for following Discord's Terms of Service and any applicable laws and regulations. diff --git a/README_torrent_tracker_bot.md b/README_torrent_tracker_bot.md new file mode 100644 index 0000000..8ae81f4 --- /dev/null +++ b/README_torrent_tracker_bot.md @@ -0,0 +1,154 @@ +# Torrent Tracker Signup Bot + +A Discord bot that monitors popular private torrent trackers for open signups and notifies subscribed users when registrations become available. + +## Features + +- **Real-time Monitoring**: Checks tracker signup pages every 5 minutes +- **User Subscriptions**: Users can subscribe to specific trackers they're interested in +- **Instant Notifications**: Sends Discord notifications when a tracker opens signups +- **Multiple Trackers**: Monitors popular trackers including RED, OPS, PTP, BTN, HDB, and AB +- **Persistent Storage**: Saves user subscriptions and tracker status between restarts + +## Monitored Trackers + +- **RED** (Redacted) - Music tracker +- **OPS** (Orpheus) - Music tracker +- **PTP** (PassThePopcorn) - Movie tracker +- **BTN** (BroadcasTheNet) - TV tracker +- **HDB** (HDBits) - HD movie/TV tracker +- **AB** (AnimeBytes) - Anime tracker + +## Setup + +### Prerequisites + +1. Python 3.8 or higher +2. A Discord bot token +3. Required Python packages (see requirements.txt) + +### Installation + +1. **Clone the repository**: + ```bash + git clone + cd Discord-Bot + ``` + +2. **Install dependencies**: + ```bash + pip install -r requirements.txt + ``` + +3. **Create a Discord Bot**: + - Go to https://discord.com/developers/applications + - Create a new application + - Go to the "Bot" section + - Create a bot and copy the token + - Enable the following bot permissions: + - Send Messages + - Read Message History + - Use Slash Commands + - Embed Links + +4. **Configure the bot**: + - Set your Discord bot token as an environment variable: + ```bash + export DISCORD_BOT_TOKEN="your_bot_token_here" + ``` + - Or edit `torrent_tracker_bot.py` and replace `'CHANGE_ME'` with your token + - Set the `TARGET_CHANNEL_ID` in the script to your desired channel ID + +5. **Run the bot**: + ```bash + python torrent_tracker_bot.py + ``` + +## Commands + +### `!help` or `!tracker help` +Shows all available commands and their usage. + +### `!trackers` +Lists all monitored trackers with their current signup status (OPEN/CLOSED). + +### `!subscribe ` +Subscribe to notifications for a specific tracker. +- Example: `!subscribe RED` +- Available trackers: RED, OPS, PTP, BTN, HDB, AB + +### `!unsubscribe ` +Unsubscribe from notifications for a specific tracker. +- Example: `!unsubscribe RED` + +### `!subscriptions` +Shows your current subscriptions and their status. + +### `!status` +Shows bot status including last check time, check interval, and statistics. + +## How It Works + +1. **Monitoring**: The bot checks each tracker's signup page every 5 minutes +2. **Detection**: It looks for common signup indicators in the HTML content +3. **Notifications**: When a tracker changes from closed to open, it notifies all subscribers +4. **Persistence**: User subscriptions and tracker status are saved to JSON files + +## Important Notes + +### Limitations + +- **Detection Accuracy**: The bot uses simple heuristics to detect open signups. Some trackers may require manual verification. +- **Rate Limiting**: The bot includes delays between requests to be respectful to tracker servers. +- **Legal Compliance**: This bot only monitors publicly accessible signup pages and does not bypass any restrictions. + +### Disclaimer + +This bot is for educational and informational purposes only. Users are responsible for: +- Following the rules and terms of service of each tracker +- Ensuring they have legitimate reasons for joining private trackers +- Respecting the communities and maintaining good ratios + +## Configuration + +### Environment Variables + +- `DISCORD_BOT_TOKEN`: Your Discord bot token +- `TARGET_CHANNEL_ID`: (Optional) Set in code - the channel where notifications are sent + +### Files Created + +- `subscriptions.json`: Stores user subscriptions +- `tracker_status.json`: Stores current tracker status + +### Customization + +You can modify the following in `torrent_tracker_bot.py`: + +- `CHECK_INTERVAL`: How often to check trackers (default: 300 seconds) +- `TRACKERS`: Add or remove trackers to monitor +- Detection logic in `check_tracker_signup()` function + +## Troubleshooting + +### Common Issues + +1. **Bot not responding**: Check that the bot has proper permissions in your Discord server +2. **No notifications**: Verify `TARGET_CHANNEL_ID` is set correctly +3. **False positives**: The detection logic may need adjustment for specific trackers + +### Logs + +The bot logs important events to the console. Check the output for error messages and status updates. + +## Contributing + +Feel free to contribute by: +- Adding support for more trackers +- Improving detection accuracy +- Adding new features +- Reporting bugs + +## License + +This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..67004c7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,23 @@ +# Discord bot dependencies +discord.py>=2.3.0 + +# HTTP client for checking tracker websites +aiohttp>=3.8.0 + +# JSON handling (built-in, but listing for clarity) +# json - built-in module + +# Async support (built-in, but listing for clarity) +# asyncio - built-in module + +# Logging (built-in, but listing for clarity) +# logging - built-in module + +# Date/time handling (built-in, but listing for clarity) +# datetime - built-in module + +# OS operations (built-in, but listing for clarity) +# os - built-in module + +# Type hints (built-in in Python 3.8+, but listing for clarity) +# typing - built-in module diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py new file mode 100644 index 0000000..18d554e --- /dev/null +++ b/torrent_tracker_bot.py @@ -0,0 +1,398 @@ +import discord +import asyncio +import aiohttp +import json +import os +from datetime import datetime, timedelta +from typing import Dict, List, Set +import logging + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Bot configuration +intents = discord.Intents.default() +intents.message_content = True +intents.messages = True +intents.guild_messages = True + +client = discord.Client(intents=intents) + +# Configuration +TARGET_CHANNEL_ID = None # Set this to your target channel ID +CHECK_INTERVAL = 300 # Check every 5 minutes (300 seconds) +SUBSCRIPTIONS_FILE = "subscriptions.json" +TRACKER_STATUS_FILE = "tracker_status.json" + +# Popular private trackers to monitor +TRACKERS = { + "RED": { + "name": "Redacted (RED)", + "url": "https://redacted.ch", + "signup_url": "https://redacted.ch/register.php", + "description": "Music tracker" + }, + "OPS": { + "name": "Orpheus (OPS)", + "url": "https://orpheus.network", + "signup_url": "https://orpheus.network/register.php", + "description": "Music tracker" + }, + "PTP": { + "name": "PassThePopcorn (PTP)", + "url": "https://passthepopcorn.me", + "signup_url": "https://passthepopcorn.me/register.php", + "description": "Movie tracker" + }, + "BTN": { + "name": "BroadcasTheNet (BTN)", + "url": "https://broadcasthe.net", + "signup_url": "https://broadcasthe.net/register.php", + "description": "TV tracker" + }, + "HDB": { + "name": "HDBits (HDB)", + "url": "https://hdbits.org", + "signup_url": "https://hdbits.org/register.php", + "description": "HD movie/TV tracker" + }, + "AB": { + "name": "AnimeBytes (AB)", + "url": "https://animebytes.tv", + "signup_url": "https://animebytes.tv/register.php", + "description": "Anime tracker" + } +} + +# Global variables +subscriptions: Dict[int, Set[str]] = {} # user_id -> set of tracker codes +tracker_status: Dict[str, bool] = {} # tracker_code -> is_open +last_check_time = None + +def load_data(): + """Load subscriptions and tracker status from files""" + global subscriptions, tracker_status + + # Load subscriptions + try: + if os.path.exists(SUBSCRIPTIONS_FILE): + with open(SUBSCRIPTIONS_FILE, 'r') as f: + data = json.load(f) + subscriptions = {int(k): set(v) for k, v in data.items()} + except Exception as e: + logger.error(f"Error loading subscriptions: {e}") + subscriptions = {} + + # Load tracker status + try: + if os.path.exists(TRACKER_STATUS_FILE): + with open(TRACKER_STATUS_FILE, 'r') as f: + tracker_status = json.load(f) + except Exception as e: + logger.error(f"Error loading tracker status: {e}") + tracker_status = {} + +def save_data(): + """Save subscriptions and tracker status to files""" + try: + # Save subscriptions + with open(SUBSCRIPTIONS_FILE, 'w') as f: + data = {str(k): list(v) for k, v in subscriptions.items()} + json.dump(data, f, indent=2) + + # Save tracker status + with open(TRACKER_STATUS_FILE, 'w') as f: + json.dump(tracker_status, f, indent=2) + except Exception as e: + logger.error(f"Error saving data: {e}") + +async def check_tracker_signup(session: aiohttp.ClientSession, tracker_code: str, tracker_info: dict) -> bool: + """ + Check if a tracker has open signups + This is a simplified implementation - in reality, you'd need to check each tracker's specific signup page + """ + try: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + + async with session.get(tracker_info['signup_url'], headers=headers, timeout=10) as response: + if response.status == 200: + text = await response.text() + # Simple heuristic - look for common signup indicators + # This would need to be customized for each tracker + signup_indicators = [ + 'registration is open', + 'sign up', + 'create account', + 'register now', + 'join us' + ] + + closed_indicators = [ + 'registration is closed', + 'invites only', + 'invitation required', + 'closed registration' + ] + + text_lower = text.lower() + + # Check for closed indicators first + for indicator in closed_indicators: + if indicator in text_lower: + return False + + # Check for open indicators + for indicator in signup_indicators: + if indicator in text_lower: + return True + + return False + else: + logger.warning(f"Failed to check {tracker_code}: HTTP {response.status}") + return False + + except Exception as e: + logger.error(f"Error checking {tracker_code}: {e}") + return False + +async def monitor_trackers(): + """Main monitoring loop""" + global last_check_time + + while True: + try: + logger.info("Checking tracker signups...") + last_check_time = datetime.now() + + async with aiohttp.ClientSession() as session: + for tracker_code, tracker_info in TRACKERS.items(): + is_open = await check_tracker_signup(session, tracker_code, tracker_info) + previous_status = tracker_status.get(tracker_code, False) + + # If status changed from closed to open, notify subscribers + if is_open and not previous_status: + await notify_subscribers(tracker_code, tracker_info) + + tracker_status[tracker_code] = is_open + + # Small delay between checks to be respectful + await asyncio.sleep(2) + + save_data() + logger.info(f"Tracker check completed. Next check in {CHECK_INTERVAL} seconds.") + + except Exception as e: + logger.error(f"Error in monitoring loop: {e}") + + await asyncio.sleep(CHECK_INTERVAL) + +async def notify_subscribers(tracker_code: str, tracker_info: dict): + """Notify all subscribers when a tracker opens""" + if not TARGET_CHANNEL_ID: + return + + channel = client.get_channel(TARGET_CHANNEL_ID) + if not channel: + logger.error(f"Could not find channel {TARGET_CHANNEL_ID}") + return + + # Create notification message + embed = discord.Embed( + title="🚨 Tracker Signup Open!", + description=f"**{tracker_info['name']}** signups are now open!", + color=0x00ff00, + timestamp=datetime.now() + ) + embed.add_field(name="Description", value=tracker_info['description'], inline=False) + embed.add_field(name="Signup URL", value=tracker_info['signup_url'], inline=False) + embed.set_footer(text="Act fast - signups may close at any time!") + + # Get all users subscribed to this tracker + subscribers = [] + for user_id, user_trackers in subscriptions.items(): + if tracker_code in user_trackers: + subscribers.append(f"<@{user_id}>") + + if subscribers: + mention_text = " ".join(subscribers) + await channel.send(f"{mention_text}", embed=embed) + else: + await channel.send(embed=embed) + +@client.event +async def on_ready(): + print(f'Torrent Tracker Bot logged in as {client.user}') + load_data() + # Start monitoring in the background + client.loop.create_task(monitor_trackers()) + +@client.event +async def on_message(message): + if message.author == client.user: + return + + # Only respond in the target channel or when mentioned + if TARGET_CHANNEL_ID and message.channel.id != TARGET_CHANNEL_ID and not client.user.mentioned_in(message): + return + + content = message.content.lower().strip() + + # Help command + if content in ['!help', '!tracker help']: + embed = discord.Embed( + title="Torrent Tracker Signup Bot Commands", + color=0x0099ff + ) + embed.add_field( + name="!trackers", + value="List all monitored trackers and their current status", + inline=False + ) + embed.add_field( + name="!subscribe ", + value="Subscribe to notifications for a specific tracker (e.g., !subscribe RED)", + inline=False + ) + embed.add_field( + name="!unsubscribe ", + value="Unsubscribe from notifications for a specific tracker", + inline=False + ) + embed.add_field( + name="!subscriptions", + value="List your current subscriptions", + inline=False + ) + embed.add_field( + name="!status", + value="Show bot status and last check time", + inline=False + ) + await message.channel.send(embed=embed) + + # List trackers + elif content == '!trackers': + embed = discord.Embed( + title="Monitored Torrent Trackers", + color=0x0099ff, + timestamp=datetime.now() + ) + + for tracker_code, tracker_info in TRACKERS.items(): + status = "🟢 OPEN" if tracker_status.get(tracker_code, False) else "🔴 CLOSED" + embed.add_field( + name=f"{tracker_code} - {tracker_info['name']}", + value=f"Status: {status}\nType: {tracker_info['description']}", + inline=True + ) + + embed.set_footer(text=f"Last checked: {last_check_time.strftime('%Y-%m-%d %H:%M:%S UTC') if last_check_time else 'Never'}") + await message.channel.send(embed=embed) + + # Subscribe to tracker + elif content.startswith('!subscribe '): + tracker_code = content.split(' ', 1)[1].upper() + + if tracker_code not in TRACKERS: + await message.channel.send(f"❌ Unknown tracker: {tracker_code}\nUse `!trackers` to see available trackers.") + return + + user_id = message.author.id + if user_id not in subscriptions: + subscriptions[user_id] = set() + + if tracker_code in subscriptions[user_id]: + await message.channel.send(f"ℹ️ You're already subscribed to {TRACKERS[tracker_code]['name']}") + else: + subscriptions[user_id].add(tracker_code) + save_data() + await message.channel.send(f"✅ Subscribed to {TRACKERS[tracker_code]['name']} notifications!") + + # Unsubscribe from tracker + elif content.startswith('!unsubscribe '): + tracker_code = content.split(' ', 1)[1].upper() + + if tracker_code not in TRACKERS: + await message.channel.send(f"❌ Unknown tracker: {tracker_code}") + return + + user_id = message.author.id + if user_id in subscriptions and tracker_code in subscriptions[user_id]: + subscriptions[user_id].remove(tracker_code) + if not subscriptions[user_id]: # Remove empty subscription sets + del subscriptions[user_id] + save_data() + await message.channel.send(f"✅ Unsubscribed from {TRACKERS[tracker_code]['name']} notifications!") + else: + await message.channel.send(f"ℹ️ You're not subscribed to {TRACKERS[tracker_code]['name']}") + + # List user subscriptions + elif content == '!subscriptions': + user_id = message.author.id + user_subs = subscriptions.get(user_id, set()) + + if not user_subs: + await message.channel.send("ℹ️ You have no active subscriptions.") + else: + embed = discord.Embed( + title="Your Subscriptions", + color=0x0099ff + ) + + for tracker_code in user_subs: + tracker_info = TRACKERS[tracker_code] + status = "🟢 OPEN" if tracker_status.get(tracker_code, False) else "🔴 CLOSED" + embed.add_field( + name=f"{tracker_code} - {tracker_info['name']}", + value=f"Status: {status}", + inline=True + ) + + await message.channel.send(embed=embed) + + # Bot status + elif content == '!status': + embed = discord.Embed( + title="Bot Status", + color=0x0099ff, + timestamp=datetime.now() + ) + + embed.add_field( + name="Last Check", + value=last_check_time.strftime('%Y-%m-%d %H:%M:%S UTC') if last_check_time else 'Never', + inline=True + ) + embed.add_field( + name="Check Interval", + value=f"{CHECK_INTERVAL} seconds", + inline=True + ) + embed.add_field( + name="Total Subscribers", + value=str(len(subscriptions)), + inline=True + ) + + open_trackers = sum(1 for status in tracker_status.values() if status) + embed.add_field( + name="Open Trackers", + value=f"{open_trackers}/{len(TRACKERS)}", + inline=True + ) + + await message.channel.send(embed=embed) + +if __name__ == "__main__": + # Replace with your bot token + TOKEN = os.getenv('DISCORD_BOT_TOKEN', 'CHANGE_ME') + + if TOKEN == 'CHANGE_ME': + print("Please set your Discord bot token!") + print("Either set the DISCORD_BOT_TOKEN environment variable or edit the script.") + exit(1) + + client.run(TOKEN) From 11616aeb9b3bf85a632863c6891863bd7da5dfa7 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:12:56 +0000 Subject: [PATCH 02/16] Fix startup validation and improve error handling - Remove guild_messages intent (not available in discord.py 2.3+) - Add startup validation and warnings for TARGET_CHANNEL_ID configuration - Improve data loading with better logging and error handling - Add input validation for subscribe/unsubscribe commands - Fix channel filtering logic when TARGET_CHANNEL_ID is None - Add informative startup messages for configuration status Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- torrent_tracker_bot.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index 18d554e..436128f 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -15,7 +15,6 @@ intents = discord.Intents.default() intents.message_content = True intents.messages = True -intents.guild_messages = True client = discord.Client(intents=intents) @@ -80,6 +79,8 @@ def load_data(): with open(SUBSCRIPTIONS_FILE, 'r') as f: data = json.load(f) subscriptions = {int(k): set(v) for k, v in data.items()} + else: + subscriptions = {} except Exception as e: logger.error(f"Error loading subscriptions: {e}") subscriptions = {} @@ -89,9 +90,13 @@ def load_data(): if os.path.exists(TRACKER_STATUS_FILE): with open(TRACKER_STATUS_FILE, 'r') as f: tracker_status = json.load(f) + else: + tracker_status = {} except Exception as e: logger.error(f"Error loading tracker status: {e}") tracker_status = {} + + logger.info(f"Loaded {len(subscriptions)} user subscriptions and status for {len(tracker_status)} trackers") def save_data(): """Save subscriptions and tracker status to files""" @@ -225,6 +230,21 @@ async def notify_subscribers(tracker_code: str, tracker_info: dict): @client.event async def on_ready(): print(f'Torrent Tracker Bot logged in as {client.user}') + + # Warn if TARGET_CHANNEL_ID is not set + if TARGET_CHANNEL_ID is None: + print("⚠️ WARNING: TARGET_CHANNEL_ID is not set!") + print(" - Notifications will not be sent") + print(" - Bot will respond in ALL channels where it has access") + print(" - Please set TARGET_CHANNEL_ID in the script") + else: + channel = client.get_channel(TARGET_CHANNEL_ID) + if channel: + print(f"✅ Target channel set to: #{channel.name} ({TARGET_CHANNEL_ID})") + else: + print(f"❌ ERROR: Could not find channel with ID {TARGET_CHANNEL_ID}") + print(" Please verify the channel ID is correct") + load_data() # Start monitoring in the background client.loop.create_task(monitor_trackers()) @@ -235,10 +255,12 @@ async def on_message(message): return # Only respond in the target channel or when mentioned - if TARGET_CHANNEL_ID and message.channel.id != TARGET_CHANNEL_ID and not client.user.mentioned_in(message): + # If TARGET_CHANNEL_ID is None, respond everywhere (with warning logged) + if TARGET_CHANNEL_ID is not None and message.channel.id != TARGET_CHANNEL_ID and not client.user.mentioned_in(message): return content = message.content.lower().strip() + args = content.split() # Help command if content in ['!help', '!tracker help']: @@ -294,7 +316,10 @@ async def on_message(message): # Subscribe to tracker elif content.startswith('!subscribe '): - tracker_code = content.split(' ', 1)[1].upper() + if len(args) < 2: + await message.channel.send("❌ Please specify a tracker code. Example: `!subscribe RED`") + return + tracker_code = args[1].upper() if tracker_code not in TRACKERS: await message.channel.send(f"❌ Unknown tracker: {tracker_code}\nUse `!trackers` to see available trackers.") @@ -313,7 +338,10 @@ async def on_message(message): # Unsubscribe from tracker elif content.startswith('!unsubscribe '): - tracker_code = content.split(' ', 1)[1].upper() + if len(args) < 2: + await message.channel.send("❌ Please specify a tracker code. Example: `!unsubscribe RED`") + return + tracker_code = args[1].upper() if tracker_code not in TRACKERS: await message.channel.send(f"❌ Unknown tracker: {tracker_code}") From 8acb09ed6f2eb297cc1e52698d3ee7054ef41604 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:14:15 +0000 Subject: [PATCH 03/16] Fix compatibility issues in both bots - Remove guild_messages intent from bot.py (not available in discord.py 2.3+) - Fix duplicate @client.event decorator in bot.py - Use asyncio.create_task instead of client.loop.create_task for better compatibility - Both bots should now start without AttributeError issues Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- bot.py | 2 -- torrent_tracker_bot.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 125a701..b76914d 100644 --- a/bot.py +++ b/bot.py @@ -6,7 +6,6 @@ intents = discord.Intents.default() intents.message_content = True # Enables reading messages intents.messages = True # Ensure this is enabled -intents.guild_messages = True # Enables receiving messages in guilds #intents.direct_messages = True # Enables receiving direct messages permissions = discord.Permissions(permissions=274877992000) permissions.read_messages = True @@ -30,7 +29,6 @@ async def on_ready(): print(f'We have logged in as {client.user}') -@client.event @client.event async def on_message(message): global awaiting_reverse_proxy_response diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index 436128f..02c1ece 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -247,7 +247,7 @@ async def on_ready(): load_data() # Start monitoring in the background - client.loop.create_task(monitor_trackers()) + asyncio.create_task(monitor_trackers()) @client.event async def on_message(message): From c1f33a3a151cd377f5a4b1887e919a9775ac18e3 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:18:04 +0000 Subject: [PATCH 04/16] Major expansion: Add Reddit monitoring and more trackers - Add Reddit r/OpenSignups monitoring with automatic post detection - Extract expiration dates and invite codes from Reddit posts - Add 6 new trackers: TorrentLeech, FeenoPeer, SeedPool, DigitalCore, Old Toons World, BakaBT - Add 4 Usenet indexers: DrunkenSlug, NZBGeek, NZBPlanet, NZBFinder - Support REDDIT as special subscription for r/OpenSignups notifications - Group trackers by type (tracker/usenet) in display - Enhanced notifications with expiration dates and invite codes - Fix timezone issues with Discord embeds - Improved command validation and error messages Bot now monitors both individual tracker websites and Reddit for comprehensive signup coverage. Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- torrent_tracker_bot.py | 372 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 334 insertions(+), 38 deletions(-) diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index 02c1ece..0e73166 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -3,8 +3,9 @@ import aiohttp import json import os -from datetime import datetime, timedelta -from typing import Dict, List, Set +import re +from datetime import datetime, timedelta, timezone +from typing import Dict, List, Set, Optional import logging # Set up logging @@ -30,48 +31,127 @@ "name": "Redacted (RED)", "url": "https://redacted.ch", "signup_url": "https://redacted.ch/register.php", - "description": "Music tracker" + "description": "Music tracker", + "type": "tracker" }, "OPS": { "name": "Orpheus (OPS)", "url": "https://orpheus.network", "signup_url": "https://orpheus.network/register.php", - "description": "Music tracker" + "description": "Music tracker", + "type": "tracker" }, "PTP": { "name": "PassThePopcorn (PTP)", "url": "https://passthepopcorn.me", "signup_url": "https://passthepopcorn.me/register.php", - "description": "Movie tracker" + "description": "Movie tracker", + "type": "tracker" }, "BTN": { "name": "BroadcasTheNet (BTN)", "url": "https://broadcasthe.net", "signup_url": "https://broadcasthe.net/register.php", - "description": "TV tracker" + "description": "TV tracker", + "type": "tracker" }, "HDB": { "name": "HDBits (HDB)", "url": "https://hdbits.org", "signup_url": "https://hdbits.org/register.php", - "description": "HD movie/TV tracker" + "description": "HD movie/TV tracker", + "type": "tracker" }, "AB": { "name": "AnimeBytes (AB)", "url": "https://animebytes.tv", "signup_url": "https://animebytes.tv/register.php", - "description": "Anime tracker" + "description": "Anime tracker", + "type": "tracker" + }, + "TL": { + "name": "TorrentLeech (TL)", + "url": "https://www.torrentleech.org", + "signup_url": "https://www.torrentleech.org/user/account/register", + "description": "General tracker", + "type": "tracker" + }, + "FNP": { + "name": "FeenoPeer (FNP)", + "url": "https://feernopeeer.com", + "signup_url": "https://feernopeeer.com/register.php", + "description": "General tracker", + "type": "tracker" + }, + "SP": { + "name": "SeedPool (SP)", + "url": "https://www.seedpool.org", + "signup_url": "https://www.seedpool.org/register.php", + "description": "General tracker", + "type": "tracker" + }, + "DC": { + "name": "DigitalCore (DC)", + "url": "https://digitalcore.club", + "signup_url": "https://digitalcore.club/register.php", + "description": "General tracker", + "type": "tracker" + }, + "OTW": { + "name": "Old Toons World (OTW)", + "url": "https://oldtoonsworld.com", + "signup_url": "https://oldtoonsworld.com/register.php", + "description": "Cartoon/Animation tracker", + "type": "tracker" + }, + "BBT": { + "name": "BakaBT (BBT)", + "url": "https://bakabt.me", + "signup_url": "https://bakabt.me/signup.php", + "description": "Anime tracker", + "type": "tracker" + }, + # Usenet Indexers + "DS": { + "name": "DrunkenSlug (DS)", + "url": "https://drunkenslug.com", + "signup_url": "https://drunkenslug.com/register", + "description": "Usenet indexer", + "type": "usenet" + }, + "GEEK": { + "name": "NZBGeek (GEEK)", + "url": "https://nzbgeek.info", + "signup_url": "https://nzbgeek.info/register.php", + "description": "Usenet indexer", + "type": "usenet" + }, + "PLANET": { + "name": "NZBPlanet (PLANET)", + "url": "https://nzbplanet.net", + "signup_url": "https://nzbplanet.net/register", + "description": "Usenet indexer", + "type": "usenet" + }, + "FINDER": { + "name": "NZBFinder (FINDER)", + "url": "https://nzbfinder.ws", + "signup_url": "https://nzbfinder.ws/register", + "description": "Usenet indexer", + "type": "usenet" } } # Global variables subscriptions: Dict[int, Set[str]] = {} # user_id -> set of tracker codes tracker_status: Dict[str, bool] = {} # tracker_code -> is_open +reddit_posts: Dict[str, dict] = {} # post_id -> post_data last_check_time = None +REDDIT_FILE = "reddit_posts.json" def load_data(): - """Load subscriptions and tracker status from files""" - global subscriptions, tracker_status + """Load subscriptions, tracker status, and reddit posts from files""" + global subscriptions, tracker_status, reddit_posts # Load subscriptions try: @@ -96,10 +176,21 @@ def load_data(): logger.error(f"Error loading tracker status: {e}") tracker_status = {} - logger.info(f"Loaded {len(subscriptions)} user subscriptions and status for {len(tracker_status)} trackers") + # Load reddit posts + try: + if os.path.exists(REDDIT_FILE): + with open(REDDIT_FILE, 'r') as f: + reddit_posts = json.load(f) + else: + reddit_posts = {} + except Exception as e: + logger.error(f"Error loading reddit posts: {e}") + reddit_posts = {} + + logger.info(f"Loaded {len(subscriptions)} user subscriptions, status for {len(tracker_status)} trackers, and {len(reddit_posts)} reddit posts") def save_data(): - """Save subscriptions and tracker status to files""" + """Save subscriptions, tracker status, and reddit posts to files""" try: # Save subscriptions with open(SUBSCRIPTIONS_FILE, 'w') as f: @@ -109,6 +200,10 @@ def save_data(): # Save tracker status with open(TRACKER_STATUS_FILE, 'w') as f: json.dump(tracker_status, f, indent=2) + + # Save reddit posts + with open(REDDIT_FILE, 'w') as f: + json.dump(reddit_posts, f, indent=2) except Exception as e: logger.error(f"Error saving data: {e}") @@ -163,16 +258,165 @@ async def check_tracker_signup(session: aiohttp.ClientSession, tracker_code: str logger.error(f"Error checking {tracker_code}: {e}") return False +async def check_reddit_opensignups(session: aiohttp.ClientSession) -> List[dict]: + """Check /r/OpenSignups for new posts""" + try: + headers = { + 'User-Agent': 'TorrentTrackerBot/1.0 (Discord Bot for signup notifications)' + } + + # Use Reddit JSON API + url = "https://www.reddit.com/r/OpenSignups/new.json?limit=25" + + async with session.get(url, headers=headers, timeout=15) as response: + if response.status == 200: + data = await response.json() + new_posts = [] + + for post in data['data']['children']: + post_data = post['data'] + post_id = post_data['id'] + + # Skip if we've already seen this post + if post_id in reddit_posts: + continue + + # Extract relevant information + title = post_data['title'] + url = post_data['url'] + selftext = post_data.get('selftext', '') + created_utc = post_data['created_utc'] + author = post_data['author'] + permalink = f"https://reddit.com{post_data['permalink']}" + + # Look for expiration dates and invite codes in title and text + full_text = f"{title} {selftext}".lower() + + # Extract expiration date patterns + expiry_patterns = [ + r'expires?\s+(?:on\s+)?(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})', + r'until\s+(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})', + r'(\d{1,2}\s+(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d{2,4})', + r'ends?\s+(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})', + r'closes?\s+(?:on\s+)?(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})' + ] + + expiry_date = None + for pattern in expiry_patterns: + match = re.search(pattern, full_text, re.IGNORECASE) + if match: + expiry_date = match.group(1) + break + + # Extract invite codes + invite_patterns = [ + r'invite\s*code[:\s]+([A-Za-z0-9]+)', + r'code[:\s]+([A-Za-z0-9]+)', + r'use[:\s]+([A-Za-z0-9]+)', + r'registration\s*code[:\s]+([A-Za-z0-9]+)' + ] + + invite_code = None + for pattern in invite_patterns: + match = re.search(pattern, full_text, re.IGNORECASE) + if match: + invite_code = match.group(1) + break + + # Determine tracker type from title + tracker_type = "tracker" # default + if any(word in full_text for word in ['usenet', 'nzb', 'indexer']): + tracker_type = "usenet" + + post_info = { + 'id': post_id, + 'title': title, + 'url': url, + 'selftext': selftext, + 'author': author, + 'permalink': permalink, + 'created_utc': created_utc, + 'expiry_date': expiry_date, + 'invite_code': invite_code, + 'tracker_type': tracker_type, + 'notified': False + } + + reddit_posts[post_id] = post_info + new_posts.append(post_info) + + return new_posts + else: + logger.warning(f"Failed to check Reddit: HTTP {response.status}") + return [] + + except Exception as e: + logger.error(f"Error checking Reddit: {e}") + return [] + +async def notify_reddit_signup(post_info: dict): + """Notify subscribers about a Reddit signup post""" + if not TARGET_CHANNEL_ID: + return + + channel = client.get_channel(TARGET_CHANNEL_ID) + if not channel: + logger.error(f"Could not find channel {TARGET_CHANNEL_ID}") + return + + # Create notification message + embed = discord.Embed( + title="🔥 New Signup from r/OpenSignups!", + description=post_info['title'], + color=0xff4500, # Reddit orange + timestamp=datetime.fromtimestamp(post_info['created_utc'], timezone.utc), + url=post_info['permalink'] + ) + + embed.add_field(name="Type", value=post_info['tracker_type'].title(), inline=True) + embed.add_field(name="Author", value=f"u/{post_info['author']}", inline=True) + + if post_info['expiry_date']: + embed.add_field(name="⏰ Expires", value=post_info['expiry_date'], inline=True) + + if post_info['invite_code']: + embed.add_field(name="🎫 Invite Code", value=f"`{post_info['invite_code']}`", inline=False) + + if post_info['selftext'] and len(post_info['selftext']) > 0: + # Truncate long text + text = post_info['selftext'][:500] + if len(post_info['selftext']) > 500: + text += "..." + embed.add_field(name="Details", value=text, inline=False) + + if post_info['url'] != post_info['permalink']: + embed.add_field(name="🔗 Direct Link", value=post_info['url'], inline=False) + + embed.set_footer(text="From r/OpenSignups • React quickly!") + + # Get all users subscribed to reddit notifications (using "REDDIT" as a special tracker code) + subscribers = [] + for user_id, user_trackers in subscriptions.items(): + if "REDDIT" in user_trackers: + subscribers.append(f"<@{user_id}>") + + if subscribers: + mention_text = " ".join(subscribers) + await channel.send(f"{mention_text}", embed=embed) + else: + await channel.send(embed=embed) + async def monitor_trackers(): """Main monitoring loop""" global last_check_time while True: try: - logger.info("Checking tracker signups...") + logger.info("Checking tracker signups and Reddit...") last_check_time = datetime.now() async with aiohttp.ClientSession() as session: + # Check individual trackers for tracker_code, tracker_info in TRACKERS.items(): is_open = await check_tracker_signup(session, tracker_code, tracker_info) previous_status = tracker_status.get(tracker_code, False) @@ -185,9 +429,15 @@ async def monitor_trackers(): # Small delay between checks to be respectful await asyncio.sleep(2) + + # Check Reddit for new posts + new_reddit_posts = await check_reddit_opensignups(session) + for post_info in new_reddit_posts: + await notify_reddit_signup(post_info) + post_info['notified'] = True save_data() - logger.info(f"Tracker check completed. Next check in {CHECK_INTERVAL} seconds.") + logger.info(f"Check completed. Found {len(new_reddit_posts) if 'new_reddit_posts' in locals() else 0} new Reddit posts. Next check in {CHECK_INTERVAL} seconds.") except Exception as e: logger.error(f"Error in monitoring loop: {e}") @@ -209,7 +459,7 @@ async def notify_subscribers(tracker_code: str, tracker_info: dict): title="🚨 Tracker Signup Open!", description=f"**{tracker_info['name']}** signups are now open!", color=0x00ff00, - timestamp=datetime.now() + timestamp=datetime.now(timezone.utc) ) embed.add_field(name="Description", value=tracker_info['description'], inline=False) embed.add_field(name="Signup URL", value=tracker_info['signup_url'], inline=False) @@ -288,6 +538,11 @@ async def on_message(message): value="List your current subscriptions", inline=False ) + embed.add_field( + name="!reddit", + value="Subscribe/unsubscribe to Reddit r/OpenSignups notifications", + inline=False + ) embed.add_field( name="!status", value="Show bot status and last check time", @@ -300,16 +555,40 @@ async def on_message(message): embed = discord.Embed( title="Monitored Torrent Trackers", color=0x0099ff, - timestamp=datetime.now() + timestamp=datetime.now(timezone.utc) ) + # Group by type + trackers_by_type = {} for tracker_code, tracker_info in TRACKERS.items(): + tracker_type = tracker_info.get('type', 'tracker') + if tracker_type not in trackers_by_type: + trackers_by_type[tracker_type] = [] + status = "🟢 OPEN" if tracker_status.get(tracker_code, False) else "🔴 CLOSED" - embed.add_field( - name=f"{tracker_code} - {tracker_info['name']}", - value=f"Status: {status}\nType: {tracker_info['description']}", - inline=True - ) + trackers_by_type[tracker_type].append({ + 'code': tracker_code, + 'name': tracker_info['name'], + 'status': status, + 'description': tracker_info['description'] + }) + + # Add trackers grouped by type + for tracker_type, trackers in trackers_by_type.items(): + type_emoji = "🎬" if tracker_type == "tracker" else "📰" + for tracker in trackers: + embed.add_field( + name=f"{type_emoji} {tracker['code']} - {tracker['name']}", + value=f"Status: {tracker['status']}\nType: {tracker['description']}", + inline=True + ) + + # Add Reddit monitoring status + embed.add_field( + name="🔥 REDDIT - r/OpenSignups", + value="Status: 🟢 MONITORING\nType: Reddit posts", + inline=True + ) embed.set_footer(text=f"Last checked: {last_check_time.strftime('%Y-%m-%d %H:%M:%S UTC') if last_check_time else 'Never'}") await message.channel.send(embed=embed) @@ -317,12 +596,12 @@ async def on_message(message): # Subscribe to tracker elif content.startswith('!subscribe '): if len(args) < 2: - await message.channel.send("❌ Please specify a tracker code. Example: `!subscribe RED`") + await message.channel.send("❌ Please specify a tracker code. Example: `!subscribe RED` or `!subscribe REDDIT`") return tracker_code = args[1].upper() - if tracker_code not in TRACKERS: - await message.channel.send(f"❌ Unknown tracker: {tracker_code}\nUse `!trackers` to see available trackers.") + if tracker_code not in TRACKERS and tracker_code != "REDDIT": + await message.channel.send(f"❌ Unknown tracker: {tracker_code}\nUse `!trackers` to see available trackers or use `REDDIT` for r/OpenSignups.") return user_id = message.author.id @@ -330,20 +609,22 @@ async def on_message(message): subscriptions[user_id] = set() if tracker_code in subscriptions[user_id]: - await message.channel.send(f"ℹ️ You're already subscribed to {TRACKERS[tracker_code]['name']}") + name = TRACKERS[tracker_code]['name'] if tracker_code in TRACKERS else "Reddit r/OpenSignups" + await message.channel.send(f"ℹ️ You're already subscribed to {name}") else: subscriptions[user_id].add(tracker_code) save_data() - await message.channel.send(f"✅ Subscribed to {TRACKERS[tracker_code]['name']} notifications!") + name = TRACKERS[tracker_code]['name'] if tracker_code in TRACKERS else "Reddit r/OpenSignups" + await message.channel.send(f"✅ Subscribed to {name} notifications!") # Unsubscribe from tracker elif content.startswith('!unsubscribe '): if len(args) < 2: - await message.channel.send("❌ Please specify a tracker code. Example: `!unsubscribe RED`") + await message.channel.send("❌ Please specify a tracker code. Example: `!unsubscribe RED` or `!unsubscribe REDDIT`") return tracker_code = args[1].upper() - if tracker_code not in TRACKERS: + if tracker_code not in TRACKERS and tracker_code != "REDDIT": await message.channel.send(f"❌ Unknown tracker: {tracker_code}") return @@ -353,9 +634,11 @@ async def on_message(message): if not subscriptions[user_id]: # Remove empty subscription sets del subscriptions[user_id] save_data() - await message.channel.send(f"✅ Unsubscribed from {TRACKERS[tracker_code]['name']} notifications!") + name = TRACKERS[tracker_code]['name'] if tracker_code in TRACKERS else "Reddit r/OpenSignups" + await message.channel.send(f"✅ Unsubscribed from {name} notifications!") else: - await message.channel.send(f"ℹ️ You're not subscribed to {TRACKERS[tracker_code]['name']}") + name = TRACKERS[tracker_code]['name'] if tracker_code in TRACKERS else "Reddit r/OpenSignups" + await message.channel.send(f"ℹ️ You're not subscribed to {name}") # List user subscriptions elif content == '!subscriptions': @@ -371,13 +654,21 @@ async def on_message(message): ) for tracker_code in user_subs: - tracker_info = TRACKERS[tracker_code] - status = "🟢 OPEN" if tracker_status.get(tracker_code, False) else "🔴 CLOSED" - embed.add_field( - name=f"{tracker_code} - {tracker_info['name']}", - value=f"Status: {status}", - inline=True - ) + if tracker_code == "REDDIT": + embed.add_field( + name="🔥 REDDIT - r/OpenSignups", + value="Status: 🟢 MONITORING", + inline=True + ) + else: + tracker_info = TRACKERS[tracker_code] + status = "🟢 OPEN" if tracker_status.get(tracker_code, False) else "🔴 CLOSED" + type_emoji = "🎬" if tracker_info.get('type') == "tracker" else "📰" + embed.add_field( + name=f"{type_emoji} {tracker_code} - {tracker_info['name']}", + value=f"Status: {status}", + inline=True + ) await message.channel.send(embed=embed) @@ -386,7 +677,7 @@ async def on_message(message): embed = discord.Embed( title="Bot Status", color=0x0099ff, - timestamp=datetime.now() + timestamp=datetime.now(timezone.utc) ) embed.add_field( @@ -411,6 +702,11 @@ async def on_message(message): value=f"{open_trackers}/{len(TRACKERS)}", inline=True ) + embed.add_field( + name="Reddit Posts Tracked", + value=str(len(reddit_posts)), + inline=True + ) await message.channel.send(embed=embed) From a31941b872a635c6c78c19bc70f3be2b04fbba0a Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:19:52 +0000 Subject: [PATCH 05/16] Fix timezone, URL typo, and help text issues - Fix last_check_time to use UTC timezone for consistency - Correct FeenoPeer URL typo (was feernopeeer.com, now feenopeer.com) - Update help text to clarify Reddit subscription commands - Remove "invites only" from closed indicators to reduce false negatives - Add "registration disabled" as closed indicator Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- torrent_tracker_bot.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index 0e73166..e796864 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -78,8 +78,8 @@ }, "FNP": { "name": "FeenoPeer (FNP)", - "url": "https://feernopeeer.com", - "signup_url": "https://feernopeeer.com/register.php", + "url": "https://feenopeer.com", + "signup_url": "https://feenopeer.com/register.php", "description": "General tracker", "type": "tracker" }, @@ -232,9 +232,9 @@ async def check_tracker_signup(session: aiohttp.ClientSession, tracker_code: str closed_indicators = [ 'registration is closed', - 'invites only', 'invitation required', - 'closed registration' + 'closed registration', + 'registration disabled' ] text_lower = text.lower() @@ -413,7 +413,7 @@ async def monitor_trackers(): while True: try: logger.info("Checking tracker signups and Reddit...") - last_check_time = datetime.now() + last_check_time = datetime.now(timezone.utc) async with aiohttp.ClientSession() as session: # Check individual trackers @@ -539,8 +539,8 @@ async def on_message(message): inline=False ) embed.add_field( - name="!reddit", - value="Subscribe/unsubscribe to Reddit r/OpenSignups notifications", + name="Reddit Monitoring", + value="Use `!subscribe REDDIT` or `!unsubscribe REDDIT` for r/OpenSignups notifications", inline=False ) embed.add_field( From d2d1098f1d181c64dfa65df399c422c533f7865f Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:22:00 +0000 Subject: [PATCH 06/16] Update documentation to reflect expanded functionality - Update README files to list all 12 trackers + 4 Usenet indexers + Reddit - Clean up requirements.txt to remove built-in modules - Update feature descriptions to mention Reddit monitoring - Add comprehensive tracker/indexer lists in documentation - Clarify Reddit subscription commands in help text Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- README.md | 7 ++++--- README_torrent_tracker_bot.md | 35 +++++++++++++++++++++++++++-------- requirements.txt | 18 ------------------ 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a52895e..231bdb4 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,12 @@ A Discord bot that provides automated support for Nextcloud-related questions. A Discord bot that monitors popular private torrent trackers for open signups and notifies users. **Features:** -- Monitors 6 popular private trackers (RED, OPS, PTP, BTN, HDB, AB) -- User subscription system +- Monitors 12 torrent trackers, 4 Usenet indexers, and Reddit r/OpenSignups +- User subscription system with Discord commands - Real-time notifications when signups open +- Extracts expiration dates and invite codes from Reddit posts - Persistent storage of subscriptions and tracker status -- Comprehensive command system +- Comprehensive command system with help documentation **Usage:** - See `README_torrent_tracker_bot.md` for detailed setup instructions diff --git a/README_torrent_tracker_bot.md b/README_torrent_tracker_bot.md index 8ae81f4..6546018 100644 --- a/README_torrent_tracker_bot.md +++ b/README_torrent_tracker_bot.md @@ -4,20 +4,37 @@ A Discord bot that monitors popular private torrent trackers for open signups an ## Features -- **Real-time Monitoring**: Checks tracker signup pages every 5 minutes -- **User Subscriptions**: Users can subscribe to specific trackers they're interested in -- **Instant Notifications**: Sends Discord notifications when a tracker opens signups -- **Multiple Trackers**: Monitors popular trackers including RED, OPS, PTP, BTN, HDB, and AB +- **Real-time Monitoring**: Checks tracker signup pages and Reddit every 5 minutes +- **User Subscriptions**: Users can subscribe to specific trackers or Reddit notifications +- **Instant Notifications**: Sends Discord notifications when signups become available +- **Multiple Sources**: Monitors 12 torrent trackers, 4 Usenet indexers, and Reddit r/OpenSignups +- **Smart Detection**: Extracts expiration dates and invite codes from Reddit posts - **Persistent Storage**: Saves user subscriptions and tracker status between restarts -## Monitored Trackers +## Monitored Sources +### Torrent Trackers - **RED** (Redacted) - Music tracker - **OPS** (Orpheus) - Music tracker - **PTP** (PassThePopcorn) - Movie tracker - **BTN** (BroadcasTheNet) - TV tracker - **HDB** (HDBits) - HD movie/TV tracker - **AB** (AnimeBytes) - Anime tracker +- **TL** (TorrentLeech) - General tracker +- **FNP** (FeenoPeer) - General tracker +- **SP** (SeedPool) - General tracker +- **DC** (DigitalCore) - General tracker +- **OTW** (Old Toons World) - Cartoon/Animation tracker +- **BBT** (BakaBT) - Anime tracker + +### Usenet Indexers +- **DS** (DrunkenSlug) - Usenet indexer +- **GEEK** (NZBGeek) - Usenet indexer +- **PLANET** (NZBPlanet) - Usenet indexer +- **FINDER** (NZBFinder) - Usenet indexer + +### Reddit Monitoring +- **r/OpenSignups** - Automatic monitoring of Reddit's OpenSignups community ## Setup @@ -73,9 +90,11 @@ Shows all available commands and their usage. Lists all monitored trackers with their current signup status (OPEN/CLOSED). ### `!subscribe ` -Subscribe to notifications for a specific tracker. -- Example: `!subscribe RED` -- Available trackers: RED, OPS, PTP, BTN, HDB, AB +Subscribe to notifications for a specific tracker or Reddit. +- Example: `!subscribe RED` or `!subscribe REDDIT` +- Available trackers: RED, OPS, PTP, BTN, HDB, AB, TL, FNP, SP, DC, OTW, BBT +- Available indexers: DS, GEEK, PLANET, FINDER +- Use `REDDIT` for r/OpenSignups notifications ### `!unsubscribe ` Unsubscribe from notifications for a specific tracker. diff --git a/requirements.txt b/requirements.txt index 67004c7..a3a39a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,21 +3,3 @@ discord.py>=2.3.0 # HTTP client for checking tracker websites aiohttp>=3.8.0 - -# JSON handling (built-in, but listing for clarity) -# json - built-in module - -# Async support (built-in, but listing for clarity) -# asyncio - built-in module - -# Logging (built-in, but listing for clarity) -# logging - built-in module - -# Date/time handling (built-in, but listing for clarity) -# datetime - built-in module - -# OS operations (built-in, but listing for clarity) -# os - built-in module - -# Type hints (built-in in Python 3.8+, but listing for clarity) -# typing - built-in module From 8ce7511fbc5c16fd1d97b797fe4868b7d18a17f9 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:23:32 +0000 Subject: [PATCH 07/16] Fix BroadcastTheNet spelling - Correct "BroadcasTheNet" to "BroadcastTheNet" in tracker name - Update both code and documentation for consistency Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- README_torrent_tracker_bot.md | 2 +- torrent_tracker_bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README_torrent_tracker_bot.md b/README_torrent_tracker_bot.md index 6546018..dc34917 100644 --- a/README_torrent_tracker_bot.md +++ b/README_torrent_tracker_bot.md @@ -17,7 +17,7 @@ A Discord bot that monitors popular private torrent trackers for open signups an - **RED** (Redacted) - Music tracker - **OPS** (Orpheus) - Music tracker - **PTP** (PassThePopcorn) - Movie tracker -- **BTN** (BroadcasTheNet) - TV tracker +- **BTN** (BroadcastTheNet) - TV tracker - **HDB** (HDBits) - HD movie/TV tracker - **AB** (AnimeBytes) - Anime tracker - **TL** (TorrentLeech) - General tracker diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index e796864..e663ac8 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -49,7 +49,7 @@ "type": "tracker" }, "BTN": { - "name": "BroadcasTheNet (BTN)", + "name": "BroadcastTheNet (BTN)", "url": "https://broadcasthe.net", "signup_url": "https://broadcasthe.net/register.php", "description": "TV tracker", From f017af5828ab07d0c9391dbb4eeba6be644958e2 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:33:40 +0000 Subject: [PATCH 08/16] Add systemd service setup for background operation - Add torrent-tracker-bot.service systemd service file - Create automated setup.sh script for easy installation - Add comprehensive SYSTEMD_SETUP.md guide - Include security hardening in service configuration - Support for virtual environment and proper permissions - Complete instructions for service management and troubleshooting Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- SYSTEMD_SETUP.md | 156 ++++++++++++++++++++++++++++++++++++ setup.sh | 60 ++++++++++++++ torrent-tracker-bot.service | 30 +++++++ 3 files changed, 246 insertions(+) create mode 100644 SYSTEMD_SETUP.md create mode 100755 setup.sh create mode 100644 torrent-tracker-bot.service diff --git a/SYSTEMD_SETUP.md b/SYSTEMD_SETUP.md new file mode 100644 index 0000000..60706fb --- /dev/null +++ b/SYSTEMD_SETUP.md @@ -0,0 +1,156 @@ +# Systemd Setup Guide for Torrent Tracker Bot + +This guide will help you set up the torrent tracker bot to run as a systemd service in the background. + +## Quick Setup + +1. **Run the setup script:** + ```bash + sudo ./setup.sh + ``` + +2. **Configure the bot:** + ```bash + nano torrent_tracker_bot.py + ``` + Set `TARGET_CHANNEL_ID` to your Discord channel ID. + +3. **Set your Discord bot token:** + ```bash + sudo systemctl edit torrent-tracker-bot + ``` + Add these lines: + ```ini + [Service] + Environment=DISCORD_BOT_TOKEN=your_actual_bot_token_here + ``` + +4. **Start the service:** + ```bash + sudo systemctl enable torrent-tracker-bot + sudo systemctl start torrent-tracker-bot + ``` + +## Manual Setup (if you prefer) + +### 1. Create Virtual Environment +```bash +cd /var/discord/trackers +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +### 2. Install Systemd Service +```bash +sudo cp torrent-tracker-bot.service /etc/systemd/system/ +sudo systemctl daemon-reload +``` + +### 3. Configure Bot Token +```bash +sudo systemctl edit torrent-tracker-bot +``` +Add: +```ini +[Service] +Environment=DISCORD_BOT_TOKEN=your_bot_token_here +``` + +### 4. Configure Channel ID +Edit `torrent_tracker_bot.py` and set: +```python +TARGET_CHANNEL_ID = 1234567890123456789 # Your channel ID +``` + +### 5. Start Service +```bash +sudo systemctl enable torrent-tracker-bot +sudo systemctl start torrent-tracker-bot +``` + +## Service Management Commands + +### Check Status +```bash +sudo systemctl status torrent-tracker-bot +``` + +### View Logs +```bash +# Live logs +sudo journalctl -u torrent-tracker-bot -f + +# Recent logs +sudo journalctl -u torrent-tracker-bot --since "1 hour ago" +``` + +### Restart Service +```bash +sudo systemctl restart torrent-tracker-bot +``` + +### Stop Service +```bash +sudo systemctl stop torrent-tracker-bot +``` + +### Disable Auto-start +```bash +sudo systemctl disable torrent-tracker-bot +``` + +## Troubleshooting + +### Bot Won't Start +1. Check logs: `sudo journalctl -u torrent-tracker-bot -n 50` +2. Verify bot token is set correctly +3. Ensure TARGET_CHANNEL_ID is configured +4. Check file permissions: `ls -la /var/discord/trackers/` + +### Permission Issues +```bash +sudo chown -R root:root /var/discord/trackers/ +sudo chmod +x /var/discord/trackers/torrent_tracker_bot.py +``` + +### Virtual Environment Issues +```bash +cd /var/discord/trackers +rm -rf venv +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +sudo systemctl restart torrent-tracker-bot +``` + +### Update Bot Code +```bash +cd /var/discord/trackers +git pull # if using git +sudo systemctl restart torrent-tracker-bot +``` + +## Security Notes + +The service file includes security hardening: +- `NoNewPrivileges=true` - Prevents privilege escalation +- `PrivateTmp=true` - Isolates /tmp directory +- `ProtectSystem=strict` - Makes most of filesystem read-only +- `ProtectHome=true` - Hides user home directories +- `ReadWritePaths=/var/discord/trackers` - Only allows writes to bot directory + +## File Locations + +- **Service file:** `/etc/systemd/system/torrent-tracker-bot.service` +- **Bot directory:** `/var/discord/trackers/` +- **Virtual environment:** `/var/discord/trackers/venv/` +- **Data files:** `/var/discord/trackers/*.json` +- **Logs:** `journalctl -u torrent-tracker-bot` + +## Getting Discord Channel ID + +1. Enable Developer Mode in Discord (User Settings → Advanced → Developer Mode) +2. Right-click on your target channel +3. Select "Copy Channel ID" +4. Use this ID in the `TARGET_CHANNEL_ID` setting diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..ba6a068 --- /dev/null +++ b/setup.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Torrent Tracker Bot Setup Script +set -e + +echo "🚀 Setting up Torrent Tracker Bot..." + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo "❌ Please run as root (sudo ./setup.sh)" + exit 1 +fi + +# Set up directories +BOT_DIR="/var/discord/trackers" +echo "📁 Using directory: $BOT_DIR" + +# Create virtual environment if it doesn't exist +if [ ! -d "$BOT_DIR/venv" ]; then + echo "🐍 Creating virtual environment..." + python3 -m venv "$BOT_DIR/venv" +else + echo "✅ Virtual environment already exists" +fi + +# Activate virtual environment and install dependencies +echo "📦 Installing dependencies..." +source "$BOT_DIR/venv/bin/activate" +pip install --upgrade pip +pip install -r "$BOT_DIR/requirements.txt" + +# Copy systemd service file +echo "⚙️ Installing systemd service..." +cp "$BOT_DIR/torrent-tracker-bot.service" /etc/systemd/system/ + +# Reload systemd +systemctl daemon-reload + +echo "" +echo "✅ Setup complete!" +echo "" +echo "📝 Next steps:" +echo "1. Edit the bot configuration:" +echo " nano $BOT_DIR/torrent_tracker_bot.py" +echo " - Set TARGET_CHANNEL_ID to your Discord channel ID" +echo "" +echo "2. Set your Discord bot token:" +echo " systemctl edit torrent-tracker-bot" +echo " Add these lines:" +echo " [Service]" +echo " Environment=DISCORD_BOT_TOKEN=your_actual_bot_token_here" +echo "" +echo "3. Start the bot:" +echo " systemctl enable torrent-tracker-bot" +echo " systemctl start torrent-tracker-bot" +echo "" +echo "4. Check status:" +echo " systemctl status torrent-tracker-bot" +echo " journalctl -u torrent-tracker-bot -f" +echo "" diff --git a/torrent-tracker-bot.service b/torrent-tracker-bot.service new file mode 100644 index 0000000..3a6957e --- /dev/null +++ b/torrent-tracker-bot.service @@ -0,0 +1,30 @@ +[Unit] +Description=Torrent Tracker Signup Bot +After=network.target +Wants=network.target + +[Service] +Type=simple +User=root +Group=root +WorkingDirectory=/var/discord/trackers +Environment=PATH=/var/discord/trackers/venv/bin +ExecStart=/var/discord/trackers/venv/bin/python torrent_tracker_bot.py +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=torrent-tracker-bot + +# Environment variables (set your Discord bot token here) +Environment=DISCORD_BOT_TOKEN=YOUR_BOT_TOKEN_HERE + +# Security settings +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/discord/trackers + +[Install] +WantedBy=multi-user.target From d8d5dcb742b61b4e83987563aef23673c97a533e Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:35:38 +0000 Subject: [PATCH 09/16] Improve setup script and add concurrent write protection - Fix setup.sh to properly handle file copying from any location - Add asyncio.Lock to prevent concurrent JSON file writes - Remove placeholder token from service file to avoid confusion - Pin dependency versions with ~= for better stability - Handle case where script is run from different directory than target Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- requirements.txt | 4 ++-- setup.sh | 23 +++++++++++++++++++-- torrent-tracker-bot.service | 4 ++-- torrent_tracker_bot.py | 40 +++++++++++++++++++------------------ 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/requirements.txt b/requirements.txt index a3a39a7..77bc1d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Discord bot dependencies -discord.py>=2.3.0 +discord.py~=2.3.0 # HTTP client for checking tracker websites -aiohttp>=3.8.0 +aiohttp~=3.8.0 diff --git a/setup.sh b/setup.sh index ba6a068..e26bbd5 100755 --- a/setup.sh +++ b/setup.sh @@ -11,9 +11,28 @@ if [ "$EUID" -ne 0 ]; then exit 1 fi -# Set up directories +# Get script directory (where the repo files are) +SCRIPT_DIR=$(dirname "$(realpath "$0")") +echo "📂 Script location: $SCRIPT_DIR" + +# Set up target directory BOT_DIR="/var/discord/trackers" -echo "📁 Using directory: $BOT_DIR" +echo "📁 Target directory: $BOT_DIR" + +# Create target directory if it doesn't exist +mkdir -p "$BOT_DIR" + +# Copy files if not already in target directory +if [ "$SCRIPT_DIR" != "$BOT_DIR" ]; then + echo "📋 Copying bot files to $BOT_DIR..." + cp "$SCRIPT_DIR"/*.py "$BOT_DIR/" + cp "$SCRIPT_DIR"/*.txt "$BOT_DIR/" + cp "$SCRIPT_DIR"/*.service "$BOT_DIR/" + cp "$SCRIPT_DIR"/*.md "$BOT_DIR/" 2>/dev/null || true + echo "✅ Files copied successfully" +else + echo "✅ Already running from target directory" +fi # Create virtual environment if it doesn't exist if [ ! -d "$BOT_DIR/venv" ]; then diff --git a/torrent-tracker-bot.service b/torrent-tracker-bot.service index 3a6957e..989b17f 100644 --- a/torrent-tracker-bot.service +++ b/torrent-tracker-bot.service @@ -16,8 +16,8 @@ StandardOutput=journal StandardError=journal SyslogIdentifier=torrent-tracker-bot -# Environment variables (set your Discord bot token here) -Environment=DISCORD_BOT_TOKEN=YOUR_BOT_TOKEN_HERE +# Environment variables (set your Discord bot token with: systemctl edit torrent-tracker-bot) +# Environment=DISCORD_BOT_TOKEN=your_bot_token_here # Security settings NoNewPrivileges=true diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index e663ac8..1086d59 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -148,6 +148,7 @@ reddit_posts: Dict[str, dict] = {} # post_id -> post_data last_check_time = None REDDIT_FILE = "reddit_posts.json" +data_lock = asyncio.Lock() # Prevent concurrent file writes def load_data(): """Load subscriptions, tracker status, and reddit posts from files""" @@ -189,23 +190,24 @@ def load_data(): logger.info(f"Loaded {len(subscriptions)} user subscriptions, status for {len(tracker_status)} trackers, and {len(reddit_posts)} reddit posts") -def save_data(): +async def save_data(): """Save subscriptions, tracker status, and reddit posts to files""" - try: - # Save subscriptions - with open(SUBSCRIPTIONS_FILE, 'w') as f: - data = {str(k): list(v) for k, v in subscriptions.items()} - json.dump(data, f, indent=2) - - # Save tracker status - with open(TRACKER_STATUS_FILE, 'w') as f: - json.dump(tracker_status, f, indent=2) - - # Save reddit posts - with open(REDDIT_FILE, 'w') as f: - json.dump(reddit_posts, f, indent=2) - except Exception as e: - logger.error(f"Error saving data: {e}") + async with data_lock: + try: + # Save subscriptions + with open(SUBSCRIPTIONS_FILE, 'w') as f: + data = {str(k): list(v) for k, v in subscriptions.items()} + json.dump(data, f, indent=2) + + # Save tracker status + with open(TRACKER_STATUS_FILE, 'w') as f: + json.dump(tracker_status, f, indent=2) + + # Save reddit posts + with open(REDDIT_FILE, 'w') as f: + json.dump(reddit_posts, f, indent=2) + except Exception as e: + logger.error(f"Error saving data: {e}") async def check_tracker_signup(session: aiohttp.ClientSession, tracker_code: str, tracker_info: dict) -> bool: """ @@ -436,7 +438,7 @@ async def monitor_trackers(): await notify_reddit_signup(post_info) post_info['notified'] = True - save_data() + await save_data() logger.info(f"Check completed. Found {len(new_reddit_posts) if 'new_reddit_posts' in locals() else 0} new Reddit posts. Next check in {CHECK_INTERVAL} seconds.") except Exception as e: @@ -613,7 +615,7 @@ async def on_message(message): await message.channel.send(f"ℹ️ You're already subscribed to {name}") else: subscriptions[user_id].add(tracker_code) - save_data() + await save_data() name = TRACKERS[tracker_code]['name'] if tracker_code in TRACKERS else "Reddit r/OpenSignups" await message.channel.send(f"✅ Subscribed to {name} notifications!") @@ -633,7 +635,7 @@ async def on_message(message): subscriptions[user_id].remove(tracker_code) if not subscriptions[user_id]: # Remove empty subscription sets del subscriptions[user_id] - save_data() + await save_data() name = TRACKERS[tracker_code]['name'] if tracker_code in TRACKERS else "Reddit r/OpenSignups" await message.channel.send(f"✅ Unsubscribed from {name} notifications!") else: From b180850ea7790893f5ab95994273dde6a08b0d8d Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:37:12 +0000 Subject: [PATCH 10/16] Fix setup script file copying robustness - Add error handling for service file copy in case of partial checkouts - Prevent script failure when optional files are missing Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index e26bbd5..3730bad 100755 --- a/setup.sh +++ b/setup.sh @@ -27,7 +27,7 @@ if [ "$SCRIPT_DIR" != "$BOT_DIR" ]; then echo "📋 Copying bot files to $BOT_DIR..." cp "$SCRIPT_DIR"/*.py "$BOT_DIR/" cp "$SCRIPT_DIR"/*.txt "$BOT_DIR/" - cp "$SCRIPT_DIR"/*.service "$BOT_DIR/" + cp "$SCRIPT_DIR"/*.service "$BOT_DIR/" 2>/dev/null || true cp "$SCRIPT_DIR"/*.md "$BOT_DIR/" 2>/dev/null || true echo "✅ Files copied successfully" else From 727854a7f9879214d62f1b39dc2987719723a4b6 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:29:57 +0000 Subject: [PATCH 11/16] Filter for English-only trackers and content - Remove non-English trackers: AnimeBytes, BakaBT, FeenoPeer, SeedPool, DigitalCore - Add language metadata to all remaining trackers - Implement English language detection for Reddit posts - Add language filtering to skip non-English Reddit content - Update documentation to reflect English-only focus - Reduce tracker count from 12 to 7 English trackers - Keep 4 English Usenet indexers and Reddit monitoring with language filtering Bot now focuses exclusively on English-language content and trackers. Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- README.md | 5 +- README_torrent_tracker_bot.md | 48 ++++++------ torrent_tracker_bot.py | 144 +++++++++++++++++++--------------- 3 files changed, 106 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 231bdb4..3916553 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,11 @@ A Discord bot that provides automated support for Nextcloud-related questions. A Discord bot that monitors popular private torrent trackers for open signups and notifies users. **Features:** -- Monitors 12 torrent trackers, 4 Usenet indexers, and Reddit r/OpenSignups +- Monitors 7 English torrent trackers, 4 English Usenet indexers, and Reddit r/OpenSignups - User subscription system with Discord commands - Real-time notifications when signups open -- Extracts expiration dates and invite codes from Reddit posts +- Extracts expiration dates and invite codes from English Reddit posts +- Language filtering to focus on English-only content - Persistent storage of subscriptions and tracker status - Comprehensive command system with help documentation diff --git a/README_torrent_tracker_bot.md b/README_torrent_tracker_bot.md index dc34917..0b7fd19 100644 --- a/README_torrent_tracker_bot.md +++ b/README_torrent_tracker_bot.md @@ -7,34 +7,30 @@ A Discord bot that monitors popular private torrent trackers for open signups an - **Real-time Monitoring**: Checks tracker signup pages and Reddit every 5 minutes - **User Subscriptions**: Users can subscribe to specific trackers or Reddit notifications - **Instant Notifications**: Sends Discord notifications when signups become available -- **Multiple Sources**: Monitors 12 torrent trackers, 4 Usenet indexers, and Reddit r/OpenSignups -- **Smart Detection**: Extracts expiration dates and invite codes from Reddit posts +- **Multiple Sources**: Monitors 7 English torrent trackers, 4 English Usenet indexers, and Reddit r/OpenSignups +- **Smart Detection**: Extracts expiration dates and invite codes from English Reddit posts +- **Language Filtering**: Only monitors English-language trackers and Reddit posts - **Persistent Storage**: Saves user subscriptions and tracker status between restarts -## Monitored Sources - -### Torrent Trackers -- **RED** (Redacted) - Music tracker -- **OPS** (Orpheus) - Music tracker -- **PTP** (PassThePopcorn) - Movie tracker -- **BTN** (BroadcastTheNet) - TV tracker -- **HDB** (HDBits) - HD movie/TV tracker -- **AB** (AnimeBytes) - Anime tracker -- **TL** (TorrentLeech) - General tracker -- **FNP** (FeenoPeer) - General tracker -- **SP** (SeedPool) - General tracker -- **DC** (DigitalCore) - General tracker -- **OTW** (Old Toons World) - Cartoon/Animation tracker -- **BBT** (BakaBT) - Anime tracker - -### Usenet Indexers -- **DS** (DrunkenSlug) - Usenet indexer -- **GEEK** (NZBGeek) - Usenet indexer -- **PLANET** (NZBPlanet) - Usenet indexer -- **FINDER** (NZBFinder) - Usenet indexer +## Monitored Sources (English Only) + +### English Torrent Trackers +- **RED** (Redacted) - English music tracker +- **OPS** (Orpheus) - English music tracker +- **PTP** (PassThePopcorn) - English movie tracker +- **BTN** (BroadcastTheNet) - English TV tracker +- **HDB** (HDBits) - English HD movie/TV tracker +- **TL** (TorrentLeech) - English general tracker +- **OTW** (Old Toons World) - English cartoon/animation tracker + +### English Usenet Indexers +- **DS** (DrunkenSlug) - English Usenet indexer +- **GEEK** (NZBGeek) - English Usenet indexer +- **PLANET** (NZBPlanet) - English Usenet indexer +- **FINDER** (NZBFinder) - English Usenet indexer ### Reddit Monitoring -- **r/OpenSignups** - Automatic monitoring of Reddit's OpenSignups community +- **r/OpenSignups** - Automatic monitoring of Reddit's OpenSignups community (English posts only) ## Setup @@ -92,8 +88,8 @@ Lists all monitored trackers with their current signup status (OPEN/CLOSED). ### `!subscribe ` Subscribe to notifications for a specific tracker or Reddit. - Example: `!subscribe RED` or `!subscribe REDDIT` -- Available trackers: RED, OPS, PTP, BTN, HDB, AB, TL, FNP, SP, DC, OTW, BBT -- Available indexers: DS, GEEK, PLANET, FINDER +- Available English trackers: RED, OPS, PTP, BTN, HDB, TL, OTW +- Available English indexers: DS, GEEK, PLANET, FINDER - Use `REDDIT` for r/OpenSignups notifications ### `!unsubscribe ` diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index 1086d59..940e344 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -25,120 +25,96 @@ SUBSCRIPTIONS_FILE = "subscriptions.json" TRACKER_STATUS_FILE = "tracker_status.json" -# Popular private trackers to monitor +# English-focused private trackers to monitor TRACKERS = { "RED": { "name": "Redacted (RED)", "url": "https://redacted.ch", "signup_url": "https://redacted.ch/register.php", - "description": "Music tracker", - "type": "tracker" + "description": "English music tracker", + "type": "tracker", + "language": "english" }, "OPS": { "name": "Orpheus (OPS)", "url": "https://orpheus.network", "signup_url": "https://orpheus.network/register.php", - "description": "Music tracker", - "type": "tracker" + "description": "English music tracker", + "type": "tracker", + "language": "english" }, "PTP": { "name": "PassThePopcorn (PTP)", "url": "https://passthepopcorn.me", "signup_url": "https://passthepopcorn.me/register.php", - "description": "Movie tracker", - "type": "tracker" + "description": "English movie tracker", + "type": "tracker", + "language": "english" }, "BTN": { "name": "BroadcastTheNet (BTN)", "url": "https://broadcasthe.net", "signup_url": "https://broadcasthe.net/register.php", - "description": "TV tracker", - "type": "tracker" + "description": "English TV tracker", + "type": "tracker", + "language": "english" }, "HDB": { "name": "HDBits (HDB)", "url": "https://hdbits.org", "signup_url": "https://hdbits.org/register.php", - "description": "HD movie/TV tracker", - "type": "tracker" - }, - "AB": { - "name": "AnimeBytes (AB)", - "url": "https://animebytes.tv", - "signup_url": "https://animebytes.tv/register.php", - "description": "Anime tracker", - "type": "tracker" + "description": "English HD movie/TV tracker", + "type": "tracker", + "language": "english" }, "TL": { "name": "TorrentLeech (TL)", "url": "https://www.torrentleech.org", "signup_url": "https://www.torrentleech.org/user/account/register", - "description": "General tracker", - "type": "tracker" - }, - "FNP": { - "name": "FeenoPeer (FNP)", - "url": "https://feenopeer.com", - "signup_url": "https://feenopeer.com/register.php", - "description": "General tracker", - "type": "tracker" - }, - "SP": { - "name": "SeedPool (SP)", - "url": "https://www.seedpool.org", - "signup_url": "https://www.seedpool.org/register.php", - "description": "General tracker", - "type": "tracker" - }, - "DC": { - "name": "DigitalCore (DC)", - "url": "https://digitalcore.club", - "signup_url": "https://digitalcore.club/register.php", - "description": "General tracker", - "type": "tracker" + "description": "English general tracker", + "type": "tracker", + "language": "english" }, "OTW": { "name": "Old Toons World (OTW)", "url": "https://oldtoonsworld.com", "signup_url": "https://oldtoonsworld.com/register.php", - "description": "Cartoon/Animation tracker", - "type": "tracker" + "description": "English cartoon/animation tracker", + "type": "tracker", + "language": "english" }, - "BBT": { - "name": "BakaBT (BBT)", - "url": "https://bakabt.me", - "signup_url": "https://bakabt.me/signup.php", - "description": "Anime tracker", - "type": "tracker" - }, - # Usenet Indexers + # English Usenet Indexers "DS": { "name": "DrunkenSlug (DS)", "url": "https://drunkenslug.com", "signup_url": "https://drunkenslug.com/register", - "description": "Usenet indexer", - "type": "usenet" + "description": "English Usenet indexer", + "type": "usenet", + "language": "english" }, "GEEK": { "name": "NZBGeek (GEEK)", "url": "https://nzbgeek.info", "signup_url": "https://nzbgeek.info/register.php", - "description": "Usenet indexer", - "type": "usenet" + "description": "English Usenet indexer", + "type": "usenet", + "language": "english" }, "PLANET": { "name": "NZBPlanet (PLANET)", "url": "https://nzbplanet.net", "signup_url": "https://nzbplanet.net/register", - "description": "Usenet indexer", - "type": "usenet" + "description": "English Usenet indexer", + "type": "usenet", + "language": "english" }, "FINDER": { "name": "NZBFinder (FINDER)", "url": "https://nzbfinder.ws", "signup_url": "https://nzbfinder.ws/register", - "description": "Usenet indexer", - "type": "usenet" + "description": "English Usenet indexer", + "type": "usenet", + "language": "english" } } @@ -260,6 +236,42 @@ async def check_tracker_signup(session: aiohttp.ClientSession, tracker_code: str logger.error(f"Error checking {tracker_code}: {e}") return False +def is_likely_english(text: str) -> bool: + """Simple heuristic to detect if text is likely English""" + if not text: + return True # Default to English for empty text + + # Common English words that are good indicators + english_indicators = [ + 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'its', 'may', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'man', 'end', 'few', 'got', 'let', 'put', 'say', 'she', 'too', 'use' + ] + + # Non-English indicators (common words in other languages) + non_english_indicators = [ + # French + 'le', 'de', 'et', 'à', 'un', 'il', 'être', 'et', 'en', 'avoir', 'que', 'pour', 'dans', 'ce', 'son', 'une', 'sur', 'avec', 'ne', 'se', 'pas', 'tout', 'plus', 'par', 'grand', 'end', 'le', 'bien', 'autre', 'comme', 'notre', 'tout', 'sans', 'peut', + # German + 'der', 'die', 'und', 'in', 'den', 'von', 'zu', 'das', 'mit', 'sich', 'des', 'auf', 'für', 'ist', 'im', 'dem', 'nicht', 'ein', 'eine', 'als', 'auch', 'es', 'an', 'werden', 'aus', 'er', 'hat', 'dass', 'sie', 'nach', 'wird', 'bei', 'einer', 'um', 'am', 'sind', 'noch', 'wie', 'einem', 'über', 'einen', 'so', 'zum', 'war', 'haben', 'nur', 'oder', 'aber', 'vor', 'zur', 'bis', 'mehr', 'durch', 'man', 'sein', 'wurde', 'sei', 'in', + # Spanish + 'el', 'la', 'de', 'que', 'y', 'a', 'en', 'un', 'es', 'se', 'no', 'te', 'lo', 'le', 'da', 'su', 'por', 'son', 'con', 'para', 'al', 'una', 'ser', 'del', 'los', 'si', 'ya', 'pero', 'más', 'o', 'este', 'sus', 'le', 'ha', 'me', 'mi', 'porque', 'qué', 'sólo', 'han', 'yo', 'hay', 'vez', 'puede', 'todos', 'así', 'nos', 'ni', 'parte', 'tiene', 'él', 'uno', 'donde', 'bien', 'tiempo', 'muy', 'cuando', 'él', 'sin', 'sobre', 'también', 'me', 'hasta', 'hay', 'donde', 'quien', 'desde', 'todo', 'nos', 'durante', 'todos', 'uno', 'les', 'ni', 'contra', 'otros', 'ese', 'eso', 'ante', 'ellos', 'e', 'esto', 'mí', 'antes', 'algunos', 'qué', 'unos', 'yo', 'otro', 'otras', 'otra', 'él', 'tanto', 'esa', 'estos', 'mucho', 'quienes', 'nada', 'muchos', 'cual', 'poco', 'ella', 'estar', 'estas', 'algunas', 'algo', 'nosotros', 'mi', 'mis', 'tú', 'te', 'ti', 'tu', 'tus', 'ellas', 'nosotras', 'vosotros', 'vosotras', 'os', 'mío', 'mía', 'míos', 'mías', 'tuyo', 'tuya', 'tuyos', 'tuyas', 'suyo', 'suya', 'suyos', 'suyas', 'nuestro', 'nuestra', 'nuestros', 'nuestras', 'vuestro', 'vuestra', 'vuestros', 'vuestras', 'esos', 'esas' + ] + + text_lower = text.lower() + words = re.findall(r'\b\w+\b', text_lower) + + if len(words) < 3: + return True # Too short to determine, assume English + + english_count = sum(1 for word in words if word in english_indicators) + non_english_count = sum(1 for word in words if word in non_english_indicators) + + # If we have a significant number of non-English indicators, it's probably not English + if non_english_count > len(words) * 0.1: # More than 10% non-English indicators + return False + + # If we have English indicators or no strong non-English indicators, assume English + return english_count > 0 or non_english_count == 0 + async def check_reddit_opensignups(session: aiohttp.ClientSession) -> List[dict]: """Check /r/OpenSignups for new posts""" try: @@ -291,8 +303,14 @@ async def check_reddit_opensignups(session: aiohttp.ClientSession) -> List[dict] author = post_data['author'] permalink = f"https://reddit.com{post_data['permalink']}" + # Check if the post is likely in English + full_text = f"{title} {selftext}" + if not is_likely_english(full_text): + logger.info(f"Skipping non-English Reddit post: {title[:50]}...") + continue + # Look for expiration dates and invite codes in title and text - full_text = f"{title} {selftext}".lower() + full_text_lower = full_text.lower() # Extract expiration date patterns expiry_patterns = [ @@ -305,7 +323,7 @@ async def check_reddit_opensignups(session: aiohttp.ClientSession) -> List[dict] expiry_date = None for pattern in expiry_patterns: - match = re.search(pattern, full_text, re.IGNORECASE) + match = re.search(pattern, full_text_lower, re.IGNORECASE) if match: expiry_date = match.group(1) break @@ -320,14 +338,14 @@ async def check_reddit_opensignups(session: aiohttp.ClientSession) -> List[dict] invite_code = None for pattern in invite_patterns: - match = re.search(pattern, full_text, re.IGNORECASE) + match = re.search(pattern, full_text_lower, re.IGNORECASE) if match: invite_code = match.group(1) break # Determine tracker type from title tracker_type = "tracker" # default - if any(word in full_text for word in ['usenet', 'nzb', 'indexer']): + if any(word in full_text_lower for word in ['usenet', 'nzb', 'indexer']): tracker_type = "usenet" post_info = { From c8e41ce1df9cd52fe26be02f1a65698364692c06 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:32:27 +0000 Subject: [PATCH 12/16] Fix language detection false positives - Remove "in" and other English words from non-English indicators - Increase threshold from 10% to 15% to reduce false positives - Expand German word list with more specific non-English terms - Improve accuracy of English language detection for Reddit posts Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- torrent_tracker_bot.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index 940e344..ec656f0 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -246,14 +246,14 @@ def is_likely_english(text: str) -> bool: 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'its', 'may', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'man', 'end', 'few', 'got', 'let', 'put', 'say', 'she', 'too', 'use' ] - # Non-English indicators (common words in other languages) + # Non-English indicators (common words in other languages, excluding English false positives) non_english_indicators = [ # French - 'le', 'de', 'et', 'à', 'un', 'il', 'être', 'et', 'en', 'avoir', 'que', 'pour', 'dans', 'ce', 'son', 'une', 'sur', 'avec', 'ne', 'se', 'pas', 'tout', 'plus', 'par', 'grand', 'end', 'le', 'bien', 'autre', 'comme', 'notre', 'tout', 'sans', 'peut', - # German - 'der', 'die', 'und', 'in', 'den', 'von', 'zu', 'das', 'mit', 'sich', 'des', 'auf', 'für', 'ist', 'im', 'dem', 'nicht', 'ein', 'eine', 'als', 'auch', 'es', 'an', 'werden', 'aus', 'er', 'hat', 'dass', 'sie', 'nach', 'wird', 'bei', 'einer', 'um', 'am', 'sind', 'noch', 'wie', 'einem', 'über', 'einen', 'so', 'zum', 'war', 'haben', 'nur', 'oder', 'aber', 'vor', 'zur', 'bis', 'mehr', 'durch', 'man', 'sein', 'wurde', 'sei', 'in', + 'le', 'de', 'et', 'à', 'un', 'il', 'être', 'avoir', 'que', 'pour', 'dans', 'ce', 'son', 'une', 'sur', 'avec', 'ne', 'se', 'pas', 'tout', 'plus', 'par', 'grand', 'bien', 'autre', 'comme', 'notre', 'sans', 'peut', 'cette', 'faire', 'leur', 'si', 'dit', 'elle', 'deux', 'même', 'lui', 'temps', 'très', 'état', 'sous', 'fait', 'lors', 'depuis', 'contre', 'lieu', 'vie', 'dont', 'fois', 'point', 'année', 'encore', 'aussi', 'alors', 'après', 'ainsi', 'où', 'tant', 'moins', 'selon', 'entre', 'pendant', 'avant', 'toujours', 'jamais', 'souvent', 'parfois', 'quelque', 'chaque', 'plusieurs', 'certains', 'autres', 'tous', 'toutes', 'aucun', 'aucune', 'quelques', 'beaucoup', 'peu', 'assez', 'trop', 'plus', 'moins', 'autant', 'tant', 'si', 'aussi', 'comme', 'que', 'quand', 'où', 'comment', 'pourquoi', 'qui', 'quoi', 'dont', 'lequel', 'laquelle', 'lesquels', 'lesquelles', + # German (excluding "in" and other English words) + 'der', 'die', 'und', 'den', 'von', 'zu', 'das', 'mit', 'sich', 'des', 'auf', 'für', 'ist', 'im', 'dem', 'nicht', 'ein', 'eine', 'als', 'auch', 'es', 'an', 'werden', 'aus', 'er', 'hat', 'dass', 'sie', 'nach', 'wird', 'bei', 'einer', 'um', 'am', 'sind', 'noch', 'wie', 'einem', 'über', 'einen', 'so', 'zum', 'war', 'haben', 'nur', 'oder', 'aber', 'vor', 'zur', 'bis', 'mehr', 'durch', 'man', 'sein', 'wurde', 'sei', 'können', 'müssen', 'sollen', 'wollen', 'dürfen', 'mögen', 'lassen', 'gehen', 'kommen', 'sehen', 'wissen', 'sagen', 'geben', 'nehmen', 'machen', 'leben', 'arbeiten', 'spielen', 'lernen', 'verstehen', 'sprechen', 'hören', 'fragen', 'antworten', 'denken', 'glauben', 'hoffen', 'wünschen', 'lieben', 'hassen', 'mögen', 'gefallen', 'helfen', 'brauchen', 'kaufen', 'verkaufen', 'bezahlen', 'kosten', 'verdienen', 'sparen', 'ausgeben', 'finden', 'suchen', 'verlieren', 'gewinnen', 'beginnen', 'aufhören', 'weitermachen', 'bleiben', 'fahren', 'fliegen', 'laufen', 'rennen', 'springen', 'fallen', 'steigen', 'sinken', 'wachsen', 'schrumpfen', 'öffnen', 'schließen', 'bauen', 'zerstören', 'reparieren', 'putzen', 'waschen', 'trocknen', 'kochen', 'essen', 'trinken', 'schlafen', 'aufwachen', 'träumen', 'lachen', 'weinen', 'lächeln', 'küssen', 'umarmen', 'berühren', 'fühlen', 'riechen', 'schmecken', 'schauen', 'beobachten', 'zeigen', 'erklären', 'lehren', 'lernen', 'studieren', 'prüfen', 'testen', 'messen', 'wiegen', 'zählen', 'rechnen', 'addieren', 'subtrahieren', 'multiplizieren', 'dividieren', 'vergleichen', 'unterscheiden', 'ähneln', 'gleichen', 'passen', 'gehören', 'besitzen', 'haben', 'bekommen', 'erhalten', 'geben', 'schenken', 'leihen', 'borgen', 'zurückgeben', 'behalten', 'wegwerfen', 'sammeln', 'ordnen', 'sortieren', 'organisieren', 'planen', 'vorbereiten', 'entscheiden', 'wählen', 'bevorzugen', 'ablehnen', 'akzeptieren', 'zustimmen', 'widersprechen', 'diskutieren', 'streiten', 'kämpfen', 'gewinnen', 'verlieren', 'siegen', 'besiegen', 'scheitern', 'erfolgreich', 'glücklich', 'traurig', 'wütend', 'ängstlich', 'nervös', 'ruhig', 'entspannt', 'müde', 'energisch', 'stark', 'schwach', 'gesund', 'krank', 'jung', 'alt', 'neu', 'alt', 'groß', 'klein', 'hoch', 'niedrig', 'lang', 'kurz', 'breit', 'schmal', 'dick', 'dünn', 'schwer', 'leicht', 'hart', 'weich', 'heiß', 'kalt', 'warm', 'kühl', 'hell', 'dunkel', 'laut', 'leise', 'schnell', 'langsam', 'früh', 'spät', 'pünktlich', 'verspätet', 'rechtzeitig', 'sofort', 'bald', 'später', 'niemals', 'immer', 'manchmal', 'oft', 'selten', 'täglich', 'wöchentlich', 'monatlich', 'jährlich', 'heute', 'gestern', 'morgen', 'übermorgen', 'vorgestern', 'jetzt', 'dann', 'damals', 'früher', 'später', 'zuerst', 'danach', 'schließlich', 'endlich', 'plötzlich', 'langsam', 'schnell', 'vorsichtig', 'sorgfältig', 'genau', 'ungefähr', 'etwa', 'fast', 'ganz', 'halb', 'voll', 'leer', 'offen', 'geschlossen', 'frei', 'besetzt', 'verfügbar', 'beschäftigt', 'fertig', 'bereit', 'möglich', 'unmöglich', 'wahrscheinlich', 'unwahrscheinlich', 'sicher', 'unsicher', 'gefährlich', 'sicher', 'einfach', 'schwierig', 'leicht', 'schwer', 'interessant', 'langweilig', 'wichtig', 'unwichtig', 'nützlich', 'nutzlos', 'notwendig', 'unnötig', 'richtig', 'falsch', 'wahr', 'unwahr', 'echt', 'falsch', 'natürlich', 'künstlich', 'normal', 'abnormal', 'gewöhnlich', 'ungewöhnlich', 'typisch', 'untypisch', 'bekannt', 'unbekannt', 'berühmt', 'unberühmt', 'beliebt', 'unbeliebt', 'freundlich', 'unfreundlich', 'höflich', 'unhöflich', 'nett', 'gemein', 'gut', 'schlecht', 'besser', 'schlechter', 'am besten', 'am schlechtesten', 'mehr', 'weniger', 'am meisten', 'am wenigsten', 'viel', 'wenig', 'genug', 'zu viel', 'zu wenig', 'alles', 'nichts', 'etwas', 'jemand', 'niemand', 'alle', 'keiner', 'einige', 'manche', 'andere', 'verschiedene', 'gleiche', 'ähnliche', 'unterschiedliche', 'dieselben', 'andere', 'nächste', 'letzte', 'erste', 'zweite', 'dritte', 'vierte', 'fünfte', 'sechste', 'siebte', 'achte', 'neunte', 'zehnte', 'elfte', 'zwölfte', 'dreizehnte', 'vierzehnte', 'fünfzehnte', 'sechzehnte', 'siebzehnte', 'achtzehnte', 'neunzehnte', 'zwanzigste', 'einundzwanzigste', 'zweiundzwanzigste', 'dreiundzwanzigste', 'vierundzwanzigste', 'fünfundzwanzigste', 'sechsundzwanzigste', 'siebenundzwanzigste', 'achtundzwanzigste', 'neunundzwanzigste', 'dreißigste', 'vierzigste', 'fünfzigste', 'sechzigste', 'siebzigste', 'achtzigste', 'neunzigste', 'hundertste', 'tausendste', 'millionste', 'milliardste', 'billionste', # Spanish - 'el', 'la', 'de', 'que', 'y', 'a', 'en', 'un', 'es', 'se', 'no', 'te', 'lo', 'le', 'da', 'su', 'por', 'son', 'con', 'para', 'al', 'una', 'ser', 'del', 'los', 'si', 'ya', 'pero', 'más', 'o', 'este', 'sus', 'le', 'ha', 'me', 'mi', 'porque', 'qué', 'sólo', 'han', 'yo', 'hay', 'vez', 'puede', 'todos', 'así', 'nos', 'ni', 'parte', 'tiene', 'él', 'uno', 'donde', 'bien', 'tiempo', 'muy', 'cuando', 'él', 'sin', 'sobre', 'también', 'me', 'hasta', 'hay', 'donde', 'quien', 'desde', 'todo', 'nos', 'durante', 'todos', 'uno', 'les', 'ni', 'contra', 'otros', 'ese', 'eso', 'ante', 'ellos', 'e', 'esto', 'mí', 'antes', 'algunos', 'qué', 'unos', 'yo', 'otro', 'otras', 'otra', 'él', 'tanto', 'esa', 'estos', 'mucho', 'quienes', 'nada', 'muchos', 'cual', 'poco', 'ella', 'estar', 'estas', 'algunas', 'algo', 'nosotros', 'mi', 'mis', 'tú', 'te', 'ti', 'tu', 'tus', 'ellas', 'nosotras', 'vosotros', 'vosotras', 'os', 'mío', 'mía', 'míos', 'mías', 'tuyo', 'tuya', 'tuyos', 'tuyas', 'suyo', 'suya', 'suyos', 'suyas', 'nuestro', 'nuestra', 'nuestros', 'nuestras', 'vuestro', 'vuestra', 'vuestros', 'vuestras', 'esos', 'esas' + 'el', 'la', 'de', 'que', 'y', 'a', 'en', 'un', 'es', 'se', 'no', 'te', 'lo', 'le', 'da', 'su', 'por', 'son', 'con', 'para', 'al', 'una', 'ser', 'del', 'los', 'si', 'ya', 'pero', 'más', 'o', 'este', 'sus', 'ha', 'me', 'mi', 'porque', 'qué', 'sólo', 'han', 'yo', 'hay', 'vez', 'puede', 'todos', 'así', 'nos', 'ni', 'parte', 'tiene', 'él', 'uno', 'donde', 'bien', 'tiempo', 'muy', 'cuando', 'sin', 'sobre', 'también', 'hasta', 'quien', 'desde', 'todo', 'durante', 'les', 'contra', 'otros', 'ese', 'eso', 'ante', 'ellos', 'e', 'esto', 'mí', 'antes', 'algunos', 'unos', 'otro', 'otras', 'otra', 'tanto', 'esa', 'estos', 'mucho', 'quienes', 'nada', 'muchos', 'cual', 'poco', 'ella', 'estar', 'estas', 'algunas', 'algo', 'nosotros', 'mis', 'tú', 'ti', 'tu', 'tus', 'ellas', 'nosotras', 'vosotros', 'vosotras', 'os', 'mío', 'mía', 'míos', 'mías', 'tuyo', 'tuya', 'tuyos', 'tuyas', 'suyo', 'suya', 'suyos', 'suyas', 'nuestro', 'nuestra', 'nuestros', 'nuestras', 'vuestro', 'vuestra', 'vuestros', 'vuestras', 'esos', 'esas' ] text_lower = text.lower() @@ -266,7 +266,7 @@ def is_likely_english(text: str) -> bool: non_english_count = sum(1 for word in words if word in non_english_indicators) # If we have a significant number of non-English indicators, it's probably not English - if non_english_count > len(words) * 0.1: # More than 10% non-English indicators + if non_english_count > len(words) * 0.15: # More than 15% non-English indicators return False # If we have English indicators or no strong non-English indicators, assume English From 6d069bd344833077898412fadfba01053a482fb9 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:34:31 +0000 Subject: [PATCH 13/16] Optimize language detection logic - Convert word lists to sets for O(1) lookup performance - Simplify logic to "assume English unless >15% non-English words" - Fix false negatives for English text with occasional foreign words - More robust and efficient language detection Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- torrent_tracker_bot.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index ec656f0..5355152 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -242,19 +242,19 @@ def is_likely_english(text: str) -> bool: return True # Default to English for empty text # Common English words that are good indicators - english_indicators = [ + english_indicators = { 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'its', 'may', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'man', 'end', 'few', 'got', 'let', 'put', 'say', 'she', 'too', 'use' - ] + } # Non-English indicators (common words in other languages, excluding English false positives) - non_english_indicators = [ + non_english_indicators = { # French 'le', 'de', 'et', 'à', 'un', 'il', 'être', 'avoir', 'que', 'pour', 'dans', 'ce', 'son', 'une', 'sur', 'avec', 'ne', 'se', 'pas', 'tout', 'plus', 'par', 'grand', 'bien', 'autre', 'comme', 'notre', 'sans', 'peut', 'cette', 'faire', 'leur', 'si', 'dit', 'elle', 'deux', 'même', 'lui', 'temps', 'très', 'état', 'sous', 'fait', 'lors', 'depuis', 'contre', 'lieu', 'vie', 'dont', 'fois', 'point', 'année', 'encore', 'aussi', 'alors', 'après', 'ainsi', 'où', 'tant', 'moins', 'selon', 'entre', 'pendant', 'avant', 'toujours', 'jamais', 'souvent', 'parfois', 'quelque', 'chaque', 'plusieurs', 'certains', 'autres', 'tous', 'toutes', 'aucun', 'aucune', 'quelques', 'beaucoup', 'peu', 'assez', 'trop', 'plus', 'moins', 'autant', 'tant', 'si', 'aussi', 'comme', 'que', 'quand', 'où', 'comment', 'pourquoi', 'qui', 'quoi', 'dont', 'lequel', 'laquelle', 'lesquels', 'lesquelles', # German (excluding "in" and other English words) 'der', 'die', 'und', 'den', 'von', 'zu', 'das', 'mit', 'sich', 'des', 'auf', 'für', 'ist', 'im', 'dem', 'nicht', 'ein', 'eine', 'als', 'auch', 'es', 'an', 'werden', 'aus', 'er', 'hat', 'dass', 'sie', 'nach', 'wird', 'bei', 'einer', 'um', 'am', 'sind', 'noch', 'wie', 'einem', 'über', 'einen', 'so', 'zum', 'war', 'haben', 'nur', 'oder', 'aber', 'vor', 'zur', 'bis', 'mehr', 'durch', 'man', 'sein', 'wurde', 'sei', 'können', 'müssen', 'sollen', 'wollen', 'dürfen', 'mögen', 'lassen', 'gehen', 'kommen', 'sehen', 'wissen', 'sagen', 'geben', 'nehmen', 'machen', 'leben', 'arbeiten', 'spielen', 'lernen', 'verstehen', 'sprechen', 'hören', 'fragen', 'antworten', 'denken', 'glauben', 'hoffen', 'wünschen', 'lieben', 'hassen', 'mögen', 'gefallen', 'helfen', 'brauchen', 'kaufen', 'verkaufen', 'bezahlen', 'kosten', 'verdienen', 'sparen', 'ausgeben', 'finden', 'suchen', 'verlieren', 'gewinnen', 'beginnen', 'aufhören', 'weitermachen', 'bleiben', 'fahren', 'fliegen', 'laufen', 'rennen', 'springen', 'fallen', 'steigen', 'sinken', 'wachsen', 'schrumpfen', 'öffnen', 'schließen', 'bauen', 'zerstören', 'reparieren', 'putzen', 'waschen', 'trocknen', 'kochen', 'essen', 'trinken', 'schlafen', 'aufwachen', 'träumen', 'lachen', 'weinen', 'lächeln', 'küssen', 'umarmen', 'berühren', 'fühlen', 'riechen', 'schmecken', 'schauen', 'beobachten', 'zeigen', 'erklären', 'lehren', 'lernen', 'studieren', 'prüfen', 'testen', 'messen', 'wiegen', 'zählen', 'rechnen', 'addieren', 'subtrahieren', 'multiplizieren', 'dividieren', 'vergleichen', 'unterscheiden', 'ähneln', 'gleichen', 'passen', 'gehören', 'besitzen', 'haben', 'bekommen', 'erhalten', 'geben', 'schenken', 'leihen', 'borgen', 'zurückgeben', 'behalten', 'wegwerfen', 'sammeln', 'ordnen', 'sortieren', 'organisieren', 'planen', 'vorbereiten', 'entscheiden', 'wählen', 'bevorzugen', 'ablehnen', 'akzeptieren', 'zustimmen', 'widersprechen', 'diskutieren', 'streiten', 'kämpfen', 'gewinnen', 'verlieren', 'siegen', 'besiegen', 'scheitern', 'erfolgreich', 'glücklich', 'traurig', 'wütend', 'ängstlich', 'nervös', 'ruhig', 'entspannt', 'müde', 'energisch', 'stark', 'schwach', 'gesund', 'krank', 'jung', 'alt', 'neu', 'alt', 'groß', 'klein', 'hoch', 'niedrig', 'lang', 'kurz', 'breit', 'schmal', 'dick', 'dünn', 'schwer', 'leicht', 'hart', 'weich', 'heiß', 'kalt', 'warm', 'kühl', 'hell', 'dunkel', 'laut', 'leise', 'schnell', 'langsam', 'früh', 'spät', 'pünktlich', 'verspätet', 'rechtzeitig', 'sofort', 'bald', 'später', 'niemals', 'immer', 'manchmal', 'oft', 'selten', 'täglich', 'wöchentlich', 'monatlich', 'jährlich', 'heute', 'gestern', 'morgen', 'übermorgen', 'vorgestern', 'jetzt', 'dann', 'damals', 'früher', 'später', 'zuerst', 'danach', 'schließlich', 'endlich', 'plötzlich', 'langsam', 'schnell', 'vorsichtig', 'sorgfältig', 'genau', 'ungefähr', 'etwa', 'fast', 'ganz', 'halb', 'voll', 'leer', 'offen', 'geschlossen', 'frei', 'besetzt', 'verfügbar', 'beschäftigt', 'fertig', 'bereit', 'möglich', 'unmöglich', 'wahrscheinlich', 'unwahrscheinlich', 'sicher', 'unsicher', 'gefährlich', 'sicher', 'einfach', 'schwierig', 'leicht', 'schwer', 'interessant', 'langweilig', 'wichtig', 'unwichtig', 'nützlich', 'nutzlos', 'notwendig', 'unnötig', 'richtig', 'falsch', 'wahr', 'unwahr', 'echt', 'falsch', 'natürlich', 'künstlich', 'normal', 'abnormal', 'gewöhnlich', 'ungewöhnlich', 'typisch', 'untypisch', 'bekannt', 'unbekannt', 'berühmt', 'unberühmt', 'beliebt', 'unbeliebt', 'freundlich', 'unfreundlich', 'höflich', 'unhöflich', 'nett', 'gemein', 'gut', 'schlecht', 'besser', 'schlechter', 'am besten', 'am schlechtesten', 'mehr', 'weniger', 'am meisten', 'am wenigsten', 'viel', 'wenig', 'genug', 'zu viel', 'zu wenig', 'alles', 'nichts', 'etwas', 'jemand', 'niemand', 'alle', 'keiner', 'einige', 'manche', 'andere', 'verschiedene', 'gleiche', 'ähnliche', 'unterschiedliche', 'dieselben', 'andere', 'nächste', 'letzte', 'erste', 'zweite', 'dritte', 'vierte', 'fünfte', 'sechste', 'siebte', 'achte', 'neunte', 'zehnte', 'elfte', 'zwölfte', 'dreizehnte', 'vierzehnte', 'fünfzehnte', 'sechzehnte', 'siebzehnte', 'achtzehnte', 'neunzehnte', 'zwanzigste', 'einundzwanzigste', 'zweiundzwanzigste', 'dreiundzwanzigste', 'vierundzwanzigste', 'fünfundzwanzigste', 'sechsundzwanzigste', 'siebenundzwanzigste', 'achtundzwanzigste', 'neunundzwanzigste', 'dreißigste', 'vierzigste', 'fünfzigste', 'sechzigste', 'siebzigste', 'achtzigste', 'neunzigste', 'hundertste', 'tausendste', 'millionste', 'milliardste', 'billionste', # Spanish 'el', 'la', 'de', 'que', 'y', 'a', 'en', 'un', 'es', 'se', 'no', 'te', 'lo', 'le', 'da', 'su', 'por', 'son', 'con', 'para', 'al', 'una', 'ser', 'del', 'los', 'si', 'ya', 'pero', 'más', 'o', 'este', 'sus', 'ha', 'me', 'mi', 'porque', 'qué', 'sólo', 'han', 'yo', 'hay', 'vez', 'puede', 'todos', 'así', 'nos', 'ni', 'parte', 'tiene', 'él', 'uno', 'donde', 'bien', 'tiempo', 'muy', 'cuando', 'sin', 'sobre', 'también', 'hasta', 'quien', 'desde', 'todo', 'durante', 'les', 'contra', 'otros', 'ese', 'eso', 'ante', 'ellos', 'e', 'esto', 'mí', 'antes', 'algunos', 'unos', 'otro', 'otras', 'otra', 'tanto', 'esa', 'estos', 'mucho', 'quienes', 'nada', 'muchos', 'cual', 'poco', 'ella', 'estar', 'estas', 'algunas', 'algo', 'nosotros', 'mis', 'tú', 'ti', 'tu', 'tus', 'ellas', 'nosotras', 'vosotros', 'vosotras', 'os', 'mío', 'mía', 'míos', 'mías', 'tuyo', 'tuya', 'tuyos', 'tuyas', 'suyo', 'suya', 'suyos', 'suyas', 'nuestro', 'nuestra', 'nuestros', 'nuestras', 'vuestro', 'vuestra', 'vuestros', 'vuestras', 'esos', 'esas' - ] + } text_lower = text.lower() words = re.findall(r'\b\w+\b', text_lower) @@ -265,12 +265,8 @@ def is_likely_english(text: str) -> bool: english_count = sum(1 for word in words if word in english_indicators) non_english_count = sum(1 for word in words if word in non_english_indicators) - # If we have a significant number of non-English indicators, it's probably not English - if non_english_count > len(words) * 0.15: # More than 15% non-English indicators - return False - - # If we have English indicators or no strong non-English indicators, assume English - return english_count > 0 or non_english_count == 0 + # Assume English unless we have a significant number of non-English indicators + return non_english_count <= len(words) * 0.15 async def check_reddit_opensignups(session: aiohttp.ClientSession) -> List[dict]: """Check /r/OpenSignups for new posts""" From 026c55c9f1ea7b0e5913390976d01fd57bdde88d Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:39:56 +0000 Subject: [PATCH 14/16] Restore all requested trackers - Add back AnimeBytes (AB), BakaBT (BBT), FeenoPeer (FNP), SeedPool (SP), DigitalCore (DC) - Keep English language filtering for Reddit posts only - Update documentation to reflect all 12 trackers + 4 indexers - Maintain language detection for Reddit while keeping all tracker sources Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- README.md | 2 +- README_torrent_tracker_bot.md | 43 +++++++++++++++++++---------------- torrent_tracker_bot.py | 40 ++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3916553..f31cfd8 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ A Discord bot that provides automated support for Nextcloud-related questions. A Discord bot that monitors popular private torrent trackers for open signups and notifies users. **Features:** -- Monitors 7 English torrent trackers, 4 English Usenet indexers, and Reddit r/OpenSignups +- Monitors 12 torrent trackers, 4 Usenet indexers, and Reddit r/OpenSignups - User subscription system with Discord commands - Real-time notifications when signups open - Extracts expiration dates and invite codes from English Reddit posts diff --git a/README_torrent_tracker_bot.md b/README_torrent_tracker_bot.md index 0b7fd19..c5e432a 100644 --- a/README_torrent_tracker_bot.md +++ b/README_torrent_tracker_bot.md @@ -7,27 +7,32 @@ A Discord bot that monitors popular private torrent trackers for open signups an - **Real-time Monitoring**: Checks tracker signup pages and Reddit every 5 minutes - **User Subscriptions**: Users can subscribe to specific trackers or Reddit notifications - **Instant Notifications**: Sends Discord notifications when signups become available -- **Multiple Sources**: Monitors 7 English torrent trackers, 4 English Usenet indexers, and Reddit r/OpenSignups +- **Multiple Sources**: Monitors 12 torrent trackers, 4 Usenet indexers, and Reddit r/OpenSignups - **Smart Detection**: Extracts expiration dates and invite codes from English Reddit posts - **Language Filtering**: Only monitors English-language trackers and Reddit posts - **Persistent Storage**: Saves user subscriptions and tracker status between restarts -## Monitored Sources (English Only) - -### English Torrent Trackers -- **RED** (Redacted) - English music tracker -- **OPS** (Orpheus) - English music tracker -- **PTP** (PassThePopcorn) - English movie tracker -- **BTN** (BroadcastTheNet) - English TV tracker -- **HDB** (HDBits) - English HD movie/TV tracker -- **TL** (TorrentLeech) - English general tracker -- **OTW** (Old Toons World) - English cartoon/animation tracker - -### English Usenet Indexers -- **DS** (DrunkenSlug) - English Usenet indexer -- **GEEK** (NZBGeek) - English Usenet indexer -- **PLANET** (NZBPlanet) - English Usenet indexer -- **FINDER** (NZBFinder) - English Usenet indexer +## Monitored Sources + +### Torrent Trackers +- **RED** (Redacted) - Music tracker +- **OPS** (Orpheus) - Music tracker +- **PTP** (PassThePopcorn) - Movie tracker +- **BTN** (BroadcastTheNet) - TV tracker +- **HDB** (HDBits) - HD movie/TV tracker +- **TL** (TorrentLeech) - General tracker +- **OTW** (Old Toons World) - Cartoon/animation tracker +- **AB** (AnimeBytes) - Anime tracker +- **BBT** (BakaBT) - Anime tracker +- **FNP** (FeenoPeer) - General tracker +- **SP** (SeedPool) - General tracker +- **DC** (DigitalCore) - General tracker + +### Usenet Indexers +- **DS** (DrunkenSlug) - Usenet indexer +- **GEEK** (NZBGeek) - Usenet indexer +- **PLANET** (NZBPlanet) - Usenet indexer +- **FINDER** (NZBFinder) - Usenet indexer ### Reddit Monitoring - **r/OpenSignups** - Automatic monitoring of Reddit's OpenSignups community (English posts only) @@ -88,8 +93,8 @@ Lists all monitored trackers with their current signup status (OPEN/CLOSED). ### `!subscribe ` Subscribe to notifications for a specific tracker or Reddit. - Example: `!subscribe RED` or `!subscribe REDDIT` -- Available English trackers: RED, OPS, PTP, BTN, HDB, TL, OTW -- Available English indexers: DS, GEEK, PLANET, FINDER +- Available trackers: RED, OPS, PTP, BTN, HDB, TL, OTW, AB, BBT, FNP, SP, DC +- Available indexers: DS, GEEK, PLANET, FINDER - Use `REDDIT` for r/OpenSignups notifications ### `!unsubscribe ` diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index 5355152..20b047d 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -83,6 +83,46 @@ "type": "tracker", "language": "english" }, + "AB": { + "name": "AnimeBytes (AB)", + "url": "https://animebytes.tv", + "signup_url": "https://animebytes.tv/register.php", + "description": "Anime tracker", + "type": "tracker", + "language": "english" + }, + "BBT": { + "name": "BakaBT (BBT)", + "url": "https://bakabt.me", + "signup_url": "https://bakabt.me/signup.php", + "description": "Anime tracker", + "type": "tracker", + "language": "english" + }, + "FNP": { + "name": "FeenoPeer (FNP)", + "url": "https://feenopeer.com", + "signup_url": "https://feenopeer.com/register.php", + "description": "General tracker", + "type": "tracker", + "language": "english" + }, + "SP": { + "name": "SeedPool (SP)", + "url": "https://www.seedpool.org", + "signup_url": "https://www.seedpool.org/register.php", + "description": "General tracker", + "type": "tracker", + "language": "english" + }, + "DC": { + "name": "DigitalCore (DC)", + "url": "https://digitalcore.club", + "signup_url": "https://digitalcore.club/register.php", + "description": "General tracker", + "type": "tracker", + "language": "english" + }, # English Usenet Indexers "DS": { "name": "DrunkenSlug (DS)", From 241c0e02808f43431ebc6d9428f81b1d020419c3 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:42:14 +0000 Subject: [PATCH 15/16] Remove unused variable in language detection - Remove unused english_count variable for cleaner code - Optimize performance by avoiding unnecessary computation - Language detection now only counts non-English indicators Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- torrent_tracker_bot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index 20b047d..92110b6 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -302,7 +302,6 @@ def is_likely_english(text: str) -> bool: if len(words) < 3: return True # Too short to determine, assume English - english_count = sum(1 for word in words if word in english_indicators) non_english_count = sum(1 for word in words if word in non_english_indicators) # Assume English unless we have a significant number of non-English indicators From 7db8a7fbc91aaffbfcc79adb70b50f92ea6e9213 Mon Sep 17 00:00:00 2001 From: MentatBot <160964065+MentatBot@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:43:50 +0000 Subject: [PATCH 16/16] Clean up unused English indicators - Remove unused english_indicators set from language detection - Simplify function by only using non-English word detection - Maintain same functionality with cleaner code Co-authored-by: TrueBankai416 <97103466+TrueBankai416@users.noreply.github.com> --- torrent_tracker_bot.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/torrent_tracker_bot.py b/torrent_tracker_bot.py index 92110b6..b906ac2 100644 --- a/torrent_tracker_bot.py +++ b/torrent_tracker_bot.py @@ -281,11 +281,6 @@ def is_likely_english(text: str) -> bool: if not text: return True # Default to English for empty text - # Common English words that are good indicators - english_indicators = { - 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'its', 'may', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'man', 'end', 'few', 'got', 'let', 'put', 'say', 'she', 'too', 'use' - } - # Non-English indicators (common words in other languages, excluding English false positives) non_english_indicators = { # French