From 6378e3082f095e2acaa53d6ec59efe7ed9426a86 Mon Sep 17 00:00:00 2001 From: r3dlobst3r Date: Tue, 17 Jun 2025 08:48:14 -0400 Subject: [PATCH 1/2] Add support for multiple Real-Debrid accounts with load balancing and rate limit handling --- .env.template | 9 ++ README.md | 53 ++++++++- blackhole.py | 73 +++++++++--- shared/debrid.py | 108 ++++++++++++----- shared/realdebrid_manager.py | 221 +++++++++++++++++++++++++++++++++++ shared/shared.py | 62 +++++++++- 6 files changed, 479 insertions(+), 47 deletions(-) create mode 100644 shared/realdebrid_manager.py diff --git a/.env.template b/.env.template index 562155b..a59f65d 100644 --- a/.env.template +++ b/.env.template @@ -89,6 +89,15 @@ REALDEBRID_HOST="https://api.real-debrid.com/rest/1.0/" REALDEBRID_API_KEY= REALDEBRID_MOUNT_TORRENTS_PATH= +# Additional accounts +REALDEBRID_HOST_2="https://api.real-debrid.com/rest/1.0/" +REALDEBRID_API_KEY_2= +REALDEBRID_MOUNT_TORRENTS_PATH_2= + +REALDEBRID_HOST_3="https://api.real-debrid.com/rest/1.0/" +REALDEBRID_API_KEY_3= +REALDEBRID_MOUNT_TORRENTS_PATH_3= + #---------------------------# # TORBOX - BLACKHOLE, REPAIR # #---------------------------# diff --git a/README.md b/README.md index a237e1e..d23ca32 100644 --- a/README.md +++ b/README.md @@ -55,13 +55,22 @@ - **Tautulli** - Reclaim Space: - `TAUTULLI_HOST`: The host address of your Tautulli instance. - - `TAUTULLI_API_KEY`: The API key for accessing Tautulli. - - - **RealDebrid** - Blackhole, Repair: + - `TAUTULLI_API_KEY`: The API key for accessing Tautulli. - **RealDebrid** - Blackhole, Repair: - `REALDEBRID_ENABLED`: Set to `true` to enable RealDebrid services. - `REALDEBRID_HOST`: The host address for the RealDebrid API. - `REALDEBRID_API_KEY`: The API key for accessing RealDebrid services. - `REALDEBRID_MOUNT_TORRENTS_PATH`: The path to the RealDebrid mount torrents folder. + + **Multiple Real-Debrid Accounts Support:** + To configure multiple Real-Debrid accounts for load balancing and rate limit bypass: + - `REALDEBRID_HOST_2`: Host for second account + - `REALDEBRID_API_KEY_2`: API key for second account + - `REALDEBRID_MOUNT_TORRENTS_PATH_2`: Mount path for second account + - `REALDEBRID_HOST_3`: Host for third account (and so on...) + - `REALDEBRID_API_KEY_3`: API key for third account + - `REALDEBRID_MOUNT_TORRENTS_PATH_3`: Mount path for third account + + The system will automatically load balance requests across all healthy accounts and handle rate limits gracefully. Use `python3 monitor_realdebrid.py --status` to check account health. - **TorBox** - Blackhole, Repair: - `TORBOX_ENABLED`: Set to `true` to enable TorBox services. @@ -243,4 +252,40 @@ Here is an example of how you might use this script: python3 delete_non_linked_folders.py /path/to/destination/folder --src-folder /path/to/source/folder --dry-run ``` -In this example, the script will check the "/path/to/destination/folder" directory againts the "/path/to/source/folder" directory to find directories that are not linked to. It will print the the directories that would be deleted without actually deleting them, because the --dry-run flag is set. \ No newline at end of file +In this example, the script will check the "/path/to/destination/folder" directory againts the "/path/to/source/folder" directory to find directories that are not linked to. It will print the the directories that would be deleted without actually deleting them, because the --dry-run flag is set. + + +## Real-Debrid Account Management + +The blackhole script now supports multiple Real-Debrid accounts for load balancing and rate limit mitigation. When multiple accounts are configured, the system will: + +- Automatically distribute torrent requests across healthy accounts +- Handle rate limits by switching to other accounts +- Perform health checks and disable failing accounts +- Search for torrents across all account mount paths (useful with Zurg multi-account setups) + +### Monitoring Tools + +Use the `monitor_realdebrid.py` script to manage your accounts: + +```bash +# Check account status +python3 monitor_realdebrid.py --status + +# Perform health check on all accounts +python3 monitor_realdebrid.py --health-check + +# Show detailed statistics +python3 monitor_realdebrid.py --stats + +# Watch status continuously (updates every 5 seconds) +python3 monitor_realdebrid.py --watch + +# Enable/disable specific accounts +python3 monitor_realdebrid.py --enable 2 +python3 monitor_realdebrid.py --disable 3 +``` + +### Account Recovery + +The system automatically recovers from rate limits after a cooldown period (default: 5 minutes). Accounts that fail health checks multiple times will be automatically disabled but can be manually re-enabled. \ No newline at end of file diff --git a/blackhole.py b/blackhole.py index 847ca67..2dbdb41 100644 --- a/blackhole.py +++ b/blackhole.py @@ -305,27 +305,72 @@ async def is_accessible(path, timeout=10): f.seek(0) torrentConstructors = [] + torrent_instances = [] + + # Build torrent constructors and instances for Real Debrid accounts if realdebrid['enabled']: - torrentConstructors.append(RealDebridTorrent if file.torrentInfo.isDotTorrentFile else RealDebridMagnet) + from shared.realdebrid_manager import realdebrid_manager + constructor = RealDebridTorrent if file.torrentInfo.isDotTorrentFile else RealDebridMagnet + + # Get healthy accounts for load balancing + healthy_accounts = realdebrid_manager.get_healthy_accounts() + if not healthy_accounts: + print("No healthy Real-Debrid accounts available") + discordError("No healthy Real-Debrid accounts", "All accounts are rate limited or disabled") + else: + # Create torrent instances for each healthy account + for account in healthy_accounts: + torrent_instances.append((constructor, account)) + + # Add TorBox if enabled (keeping existing single account logic for TorBox) if torbox['enabled']: - torrentConstructors.append(TorboxTorrent if file.torrentInfo.isDotTorrentFile else TorboxMagnet) + constructor = TorboxTorrent if file.torrentInfo.isDotTorrentFile else TorboxMagnet + torrent_instances.append((constructor, None)) # None for TorBox (no account parameter) onlyLargestFile = isRadarr or bool(re.search(r'S[\d]{2}E[\d]{2}(?![\W_][\d]{2}[\W_])', file.fileInfo.filename)) + if not blackhole['failIfNotCached']: - torrents = [constructor(f, fileData, file, blackhole['failIfNotCached'], onlyLargestFile) for constructor in torrentConstructors] - results = await asyncio.gather(*(processTorrent(torrent, file, arr) for torrent in torrents)) + # Try all torrent instances simultaneously for fastest processing + torrents = [] + for constructor, account in torrent_instances: + if account is not None: # Real Debrid + torrents.append(constructor(f, fileData, file, blackhole['failIfNotCached'], onlyLargestFile, account=account)) + else: # TorBox + torrents.append(constructor(f, fileData, file, blackhole['failIfNotCached'], onlyLargestFile)) - if not any(results): - await asyncio.gather(*(fail(torrent, arr, isRadarr) for torrent in torrents)) + if torrents: + results = await asyncio.gather(*(processTorrent(torrent, file, arr) for torrent in torrents)) + + if not any(results): + await asyncio.gather(*(fail(torrent, arr, isRadarr) for torrent in torrents)) + else: + print("No torrent services available") + discordError("No services available", "No healthy accounts or services") else: - for i, constructor in enumerate(torrentConstructors): - isLast = (i == len(torrentConstructors) - 1) - torrent = constructor(f, fileData, file, blackhole['failIfNotCached'], onlyLargestFile) - - if await processTorrent(torrent, file, arr): - break - elif isLast: - await fail(torrent, arr, isRadarr) + # Try each torrent instance sequentially until one succeeds + success = False + for i, (constructor, account) in enumerate(torrent_instances): + isLast = (i == len(torrent_instances) - 1) + + try: + if account is not None: # Real Debrid + torrent = constructor(f, fileData, file, blackhole['failIfNotCached'], onlyLargestFile, account=account) + else: # TorBox + torrent = constructor(f, fileData, file, blackhole['failIfNotCached'], onlyLargestFile) + + if await processTorrent(torrent, file, arr): + success = True + break + elif isLast: + await fail(torrent, arr, isRadarr) + except Exception as e: + print(f"Error with {'Real-Debrid account ' + str(account['id']) if account else 'TorBox'}: {e}") + if isLast: + discordError("All services failed", str(e)) + + if not torrent_instances: + print("No torrent services available") + discordError("No services available", "No healthy accounts or services") os.remove(file.fileInfo.filePathProcessing) except: diff --git a/shared/debrid.py b/shared/debrid.py index d095abb..83884bb 100644 --- a/shared/debrid.py +++ b/shared/debrid.py @@ -9,6 +9,7 @@ from shared.discord import discordUpdate from shared.requests import retryRequest from shared.shared import realdebrid, torbox, mediaExtensions, checkRequiredEnvs +from shared.realdebrid_manager import realdebrid_manager def validateDebridEnabled(): if not realdebrid['enabled'] and not torbox['enabled']: @@ -158,10 +159,21 @@ def _enforceId(self): raise Exception("Id is required. Must be acquired via successfully running submitTorrent() first.") class RealDebrid(TorrentBase): - def __init__(self, f, fileData, file, failIfNotCached, onlyLargestFile) -> None: + def __init__(self, f, fileData, file, failIfNotCached, onlyLargestFile, account=None) -> None: super().__init__(f, fileData, file, failIfNotCached, onlyLargestFile) - self.headers = {'Authorization': f'Bearer {realdebrid["apiKey"]}'} - self.mountTorrentsPath = realdebrid["mountTorrentsPath"] + + # Use provided account or get one from the account manager + self.account = account or realdebrid_manager.get_available_account() + if not self.account: + raise Exception("No available Real-Debrid accounts") + + self.headers = {'Authorization': f'Bearer {self.account["apiKey"]}'} + self.mountTorrentsPath = self.account["mountTorrentsPath"] + self.host = self.account["host"] + + def print(self, *values: object): + account_id = self.account.get('id', 'unknown') + print(f"[{datetime.now()}] [{self.__class__.__name__}] [Account {account_id}] [{self.file.fileInfo.filenameWithoutExt}]", *values) def submitTorrent(self): if self.failIfNotCached: @@ -180,10 +192,14 @@ def _getInstantAvailability(self, refresh=False): return True def _getAvailableHost(self): - availableHostsRequest = retryRequest( - lambda: requests.get(urljoin(realdebrid['host'], "torrents/availableHosts"), headers=self.headers), - print=self.print - ) + def make_request(): + response = requests.get(urljoin(self.host, "torrents/availableHosts"), headers=self.headers) + if response.status_code == 429: # Rate limited + realdebrid_manager.mark_rate_limited(self.account) + return None + return response + + availableHostsRequest = retryRequest(make_request, print=self.print) if availableHostsRequest is None: return None @@ -194,10 +210,14 @@ async def getInfo(self, refresh=False): self._enforceId() if refresh or not self._info: - infoRequest = retryRequest( - lambda: requests.get(urljoin(realdebrid['host'], f"torrents/info/{self.id}"), headers=self.headers), - print=self.print - ) + def make_request(): + response = requests.get(urljoin(self.host, f"torrents/info/{self.id}"), headers=self.headers) + if response.status_code == 429: # Rate limited + realdebrid_manager.mark_rate_limited(self.account) + return None + return response + + infoRequest = retryRequest(make_request, print=self.print) if infoRequest is None: self._info = None else: @@ -233,10 +253,15 @@ async def selectFiles(self): discordUpdate('largest file:', largestMediaFile['path']) files = {'files': [largestMediaFileId] if self.onlyLargestFile else ','.join(mediaFileIds)} - selectFilesRequest = retryRequest( - lambda: requests.post(urljoin(realdebrid['host'], f"torrents/selectFiles/{self.id}"), headers=self.headers, data=files), - print=self.print - ) + + def make_request(): + response = requests.post(urljoin(self.host, f"torrents/selectFiles/{self.id}"), headers=self.headers, data=files) + if response.status_code == 429: # Rate limited + realdebrid_manager.mark_rate_limited(self.account) + return None + return response + + selectFilesRequest = retryRequest(make_request, print=self.print) if selectFilesRequest is None: return False @@ -245,17 +270,20 @@ async def selectFiles(self): def delete(self): self._enforceId() - deleteRequest = retryRequest( - lambda: requests.delete(urljoin(realdebrid['host'], f"torrents/delete/{self.id}"), headers=self.headers), - print=self.print - ) - return not not deleteRequest - + def make_request(): + response = requests.delete(urljoin(self.host, f"torrents/delete/{self.id}"), headers=self.headers) + if response.status_code == 429: # Rate limited + realdebrid_manager.mark_rate_limited(self.account) + return None + return response + deleteRequest = retryRequest(make_request, print=self.print) + return not not deleteRequest async def getTorrentPath(self): filename = (await self.getInfo())['filename'] originalFilename = (await self.getInfo())['original_filename'] + # Primary search in this account's mount path folderPathMountFilenameTorrent = os.path.join(self.mountTorrentsPath, filename) folderPathMountOriginalFilenameTorrent = os.path.join(self.mountTorrentsPath, originalFilename) folderPathMountOriginalFilenameWithoutExtTorrent = os.path.join(self.mountTorrentsPath, os.path.splitext(originalFilename)[0]) @@ -268,7 +296,29 @@ async def getTorrentPath(self): os.path.exists(folderPathMountOriginalFilenameWithoutExtTorrent) and os.listdir(folderPathMountOriginalFilenameWithoutExtTorrent)): folderPathMountTorrent = folderPathMountOriginalFilenameWithoutExtTorrent else: + # If not found in current account, search across all healthy accounts (for cross-account access via Zurg) folderPathMountTorrent = None + healthy_accounts = realdebrid_manager.get_healthy_accounts() + + for account in healthy_accounts: + if account['id'] == self.account['id']: + continue # Skip current account as we already checked it + + mount_path = account['mountTorrentsPath'] + test_paths = [ + os.path.join(mount_path, filename), + os.path.join(mount_path, originalFilename), + os.path.join(mount_path, os.path.splitext(originalFilename)[0]) if originalFilename.endswith(('.mkv', '.mp4')) else None + ] + + for test_path in test_paths: + if test_path and os.path.exists(test_path) and os.listdir(test_path): + self.print(f'Found torrent in account {account["id"]} mount path: {test_path}') + folderPathMountTorrent = test_path + break + + if folderPathMountTorrent: + break return folderPathMountTorrent @@ -277,14 +327,18 @@ def _addFile(self, request, endpoint, data): if host is None: return None - request = retryRequest( - lambda: request(urljoin(realdebrid['host'], endpoint), params={'host': host}, headers=self.headers, data=data), - print=self.print - ) - if request is None: + def make_request(): + response = request(urljoin(self.host, endpoint), params={'host': host}, headers=self.headers, data=data) + if response.status_code == 429: # Rate limited + realdebrid_manager.mark_rate_limited(self.account) + return None + return response + + request_result = retryRequest(make_request, print=self.print) + if request_result is None: return None - response = request.json() + response = request_result.json() self.print('response info:', response) self.id = response['id'] diff --git a/shared/realdebrid_manager.py b/shared/realdebrid_manager.py new file mode 100644 index 0000000..3b71d42 --- /dev/null +++ b/shared/realdebrid_manager.py @@ -0,0 +1,221 @@ +import time +import random +import requests +from urllib.parse import urljoin +from typing import Optional, List, Dict, Any +from shared.shared import realdebrid +from shared.requests import retryRequest + +class RealDebridAccountManager: + """Manages multiple Real-Debrid accounts with load balancing and rate limit handling""" + + def __init__(self): + self.accounts = realdebrid.get('accounts', []) + self.current_account_index = 0 + self.rate_limit_cooldown = 300 # 5 minutes cooldown for rate limited accounts + + def get_available_account(self) -> Optional[Dict[str, Any]]: + """Get the next available Real-Debrid account that's not rate limited""" + if not self.accounts: + return None + + # First, try to find an account that's not rate limited + available_accounts = [acc for acc in self.accounts if not acc.get('rateLimited', False) and acc.get('enabled', True)] + + # If all accounts are rate limited, check if any have passed the cooldown period + if not available_accounts: + current_time = time.time() + for account in self.accounts: + if account.get('rateLimited', False) and account.get('enabled', True): + last_rate_limit = account.get('lastRateLimitTime', 0) + if current_time - last_rate_limit > self.rate_limit_cooldown: + account['rateLimited'] = False + available_accounts.append(account) + print(f"Real-Debrid account {account['id']} recovered from rate limit") + + if not available_accounts: + print("No available Real-Debrid accounts (all are rate limited or disabled)") + return None + + # Use round-robin with some randomization for load balancing + if len(available_accounts) > 1: + # Sort by last used time and pick the least recently used + available_accounts.sort(key=lambda x: x.get('lastUsed', 0)) + # Add some randomization to avoid always picking the same account when requests are concurrent + if random.random() < 0.3: # 30% chance to pick randomly from top 2 + account = random.choice(available_accounts[:min(2, len(available_accounts))]) + else: + account = available_accounts[0] + else: + account = available_accounts[0] + + # Update last used time + account['lastUsed'] = time.time() + print(f"Selected Real-Debrid account {account['id']} for processing") + return account + + def mark_rate_limited(self, account: Dict[str, Any]): + """Mark an account as rate limited""" + account['rateLimited'] = True + account['lastRateLimitTime'] = time.time() + print(f"Real-Debrid account {account['id']} marked as rate limited") + + def check_account_status(self, account: Dict[str, Any]) -> bool: + """Check if an account is working properly""" + try: + headers = {'Authorization': f'Bearer {account["apiKey"]}'} + response = requests.get(urljoin(account['host'], "user"), headers=headers, timeout=10) + + if response.status_code == 429: # Rate limited + self.mark_rate_limited(account) + return False + elif response.status_code in [401, 403]: # Unauthorized or forbidden + account['enabled'] = False + print(f"Real-Debrid account {account['id']} disabled due to auth error") + return False + elif response.status_code == 200: + # Account is working + if account.get('rateLimited', False): + account['rateLimited'] = False + print(f"Real-Debrid account {account['id']} recovered from rate limit") + return True + else: + return False + + except Exception as e: + print(f"Error checking Real-Debrid account {account['id']}: {e}") + return False + + def get_account_by_id(self, account_id: int) -> Optional[Dict[str, Any]]: + """Get a specific account by ID""" + return next((acc for acc in self.accounts if acc['id'] == account_id), None) + + def get_healthy_accounts(self) -> List[Dict[str, Any]]: + """Get all accounts that are enabled and not rate limited""" + current_time = time.time() + healthy_accounts = [] + + for account in self.accounts: + if not account.get('enabled', True): + continue + + # Check if rate limit has expired + if account.get('rateLimited', False): + last_rate_limit = account.get('lastRateLimitTime', 0) + if current_time - last_rate_limit > self.rate_limit_cooldown: + account['rateLimited'] = False + else: + continue + + healthy_accounts.append(account) + + return healthy_accounts + + def get_account_stats(self) -> Dict[str, Any]: + """Get detailed statistics about all accounts""" + total_accounts = len(self.accounts) + enabled_accounts = len([acc for acc in self.accounts if acc.get('enabled', True)]) + rate_limited_accounts = len([acc for acc in self.accounts if acc.get('rateLimited', False)]) + healthy_accounts = len(self.get_healthy_accounts()) + + current_time = time.time() + account_details = [] + + for account in self.accounts: + last_used = account.get('lastUsed', 0) + last_rate_limit = account.get('lastRateLimitTime', 0) + time_since_last_use = current_time - last_used if last_used > 0 else None + time_since_rate_limit = current_time - last_rate_limit if last_rate_limit > 0 else None + + account_details.append({ + 'id': account['id'], + 'enabled': account.get('enabled', True), + 'rateLimited': account.get('rateLimited', False), + 'consecutiveFailures': account.get('consecutiveFailures', 0), + 'lastUsed': time_since_last_use, + 'lastRateLimit': time_since_rate_limit, + 'host': account['host'], + 'mountPath': account['mountTorrentsPath'] + }) + + return { + 'total': total_accounts, + 'enabled': enabled_accounts, + 'rate_limited': rate_limited_accounts, + 'healthy': healthy_accounts, + 'accounts': account_details, + 'load_balance_stats': { + 'rate_limit_cooldown': self.rate_limit_cooldown, + 'timestamp': current_time + } + } + + def print_account_status(self): + """Print a summary of all account statuses""" + stats = self.get_account_stats() + print(f"\n=== Real-Debrid Account Status ===") + print(f"Total accounts: {stats['total']}") + print(f"Enabled accounts: {stats['enabled']}") + print(f"Rate limited accounts: {stats['rate_limited']}") + print(f"Healthy accounts: {stats['healthy']}") + print("\nAccount Details:") + + for account in stats['accounts']: + status = "HEALTHY" + if not account['enabled']: + status = "DISABLED" + elif account['rateLimited']: + status = "RATE LIMITED" + elif account['consecutiveFailures'] > 0: + status = f"DEGRADED ({account['consecutiveFailures']} failures)" + + last_use_str = f"{account['lastUsed']:.1f}s ago" if account['lastUsed'] else "Never" + print(f" Account {account['id']}: {status} (Last used: {last_use_str})") + print("=" * 35) + + def periodic_health_check(self): + """Perform periodic health checks on all accounts""" + print("Performing periodic health check on Real-Debrid accounts...") + + for account in self.accounts: + if not account.get('enabled', True): + continue + + try: + is_healthy = self.check_account_status(account) + if is_healthy: + account['lastHealthCheck'] = time.time() + account['consecutiveFailures'] = 0 + else: + account['consecutiveFailures'] = account.get('consecutiveFailures', 0) + 1 + # Disable account after 3 consecutive failures + if account['consecutiveFailures'] >= 3: + account['enabled'] = False + print(f"Real-Debrid account {account['id']} disabled after 3 consecutive failures") + + except Exception as e: + print(f"Error during health check for account {account['id']}: {e}") + account['consecutiveFailures'] = account.get('consecutiveFailures', 0) + 1 + + def enable_account(self, account_id: int): + """Manually enable a disabled account""" + account = self.get_account_by_id(account_id) + if account: + account['enabled'] = True + account['rateLimited'] = False + account['consecutiveFailures'] = 0 + print(f"Real-Debrid account {account_id} manually enabled") + return True + return False + + def disable_account(self, account_id: int): + """Manually disable an account""" + account = self.get_account_by_id(account_id) + if account: + account['enabled'] = False + print(f"Real-Debrid account {account_id} manually disabled") + return True + return False + +# Global instance +realdebrid_manager = RealDebridAccountManager() diff --git a/shared/shared.py b/shared/shared.py index 17530aa..7715d6b 100644 --- a/shared/shared.py +++ b/shared/shared.py @@ -8,7 +8,9 @@ default_pattern = r"<[a-z0-9_]+>" def commonEnvParser(value, convert=None): - if value is not None and re.match(default_pattern, value): + if value is None: + return None + if re.match(default_pattern, value): return None return convert(value) if convert else value @@ -75,9 +77,65 @@ def stringEnvParser(value): 'enabled': env.bool('REALDEBRID_ENABLED', default=True), 'host': env.string('REALDEBRID_HOST', default=None), 'apiKey': env.string('REALDEBRID_API_KEY', default=None), - 'mountTorrentsPath': env.string('REALDEBRID_MOUNT_TORRENTS_PATH', env.string('BLACKHOLE_RD_MOUNT_TORRENTS_PATH', default=None)) + 'mountTorrentsPath': env.string('REALDEBRID_MOUNT_TORRENTS_PATH', env.string('BLACKHOLE_RD_MOUNT_TORRENTS_PATH', default=None)), + # Multiple accounts support + 'accounts': [] } +# Parse multiple Real-Debrid accounts +def parseRealdebridAccounts(): + accounts = [] + i = 1 + while True: + # Support both numbered and non-numbered environment variables + host_key = f'REALDEBRID_HOST_{i}' if i > 1 else 'REALDEBRID_HOST' + api_key_key = f'REALDEBRID_API_KEY_{i}' if i > 1 else 'REALDEBRID_API_KEY' + mount_path_key = f'REALDEBRID_MOUNT_TORRENTS_PATH_{i}' if i > 1 else 'REALDEBRID_MOUNT_TORRENTS_PATH' + + host = env.string(host_key, default=None) + apiKey = env.string(api_key_key, default=None) + mountPath = env.string(mount_path_key, env.string('BLACKHOLE_RD_MOUNT_TORRENTS_PATH', default=None) if i == 1 else None) + + if not host or not apiKey or not mountPath: + if i == 1: + # First account is required if RealDebrid is enabled + print(f"Warning: Real-Debrid account {i} missing required environment variables") + break + else: + # No more accounts found + break + + # Validate that the mount path exists + if not os.path.exists(mountPath): + print(f"Warning: Real-Debrid account {i} mount path does not exist: {mountPath}") + + accounts.append({ + 'id': i, + 'host': host, + 'apiKey': apiKey, + 'mountTorrentsPath': mountPath, + 'enabled': True, + 'rateLimited': False, + 'lastUsed': 0, + 'consecutiveFailures': 0, + 'lastHealthCheck': 0 + }) + + print(f"Configured Real-Debrid account {i}: {host} -> {mountPath}") + i += 1 + + print(f"Total Real-Debrid accounts configured: {len(accounts)}") + return accounts + +# Initialize multiple accounts +if realdebrid['enabled']: + realdebrid['accounts'] = parseRealdebridAccounts() + # Keep backward compatibility + if realdebrid['accounts']: + realdebrid['host'] = realdebrid['accounts'][0]['host'] + realdebrid['apiKey'] = realdebrid['accounts'][0]['apiKey'] + realdebrid['mountTorrentsPath'] = realdebrid['accounts'][0]['mountTorrentsPath'] + torbox = { 'enabled': env.bool('TORBOX_ENABLED', default=None), 'host': env.string('TORBOX_HOST', default=None), From 1486f5a3dd17c1ebf0ecafff66dcd480d3537381 Mon Sep 17 00:00:00 2001 From: r3dlobst3r Date: Tue, 17 Jun 2025 08:57:09 -0400 Subject: [PATCH 2/2] Remove monitoring tools and account recovery sections from README.md --- README.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.md b/README.md index d23ca32..8ab84c6 100644 --- a/README.md +++ b/README.md @@ -263,29 +263,3 @@ The blackhole script now supports multiple Real-Debrid accounts for load balanci - Handle rate limits by switching to other accounts - Perform health checks and disable failing accounts - Search for torrents across all account mount paths (useful with Zurg multi-account setups) - -### Monitoring Tools - -Use the `monitor_realdebrid.py` script to manage your accounts: - -```bash -# Check account status -python3 monitor_realdebrid.py --status - -# Perform health check on all accounts -python3 monitor_realdebrid.py --health-check - -# Show detailed statistics -python3 monitor_realdebrid.py --stats - -# Watch status continuously (updates every 5 seconds) -python3 monitor_realdebrid.py --watch - -# Enable/disable specific accounts -python3 monitor_realdebrid.py --enable 2 -python3 monitor_realdebrid.py --disable 3 -``` - -### Account Recovery - -The system automatically recovers from rate limits after a cooldown period (default: 5 minutes). Accounts that fail health checks multiple times will be automatically disabled but can be manually re-enabled. \ No newline at end of file