""" AUTARCH Adult Site Scanner Module Username OSINT for adult-oriented platforms Searches usernames across adult content sites, fanfiction platforms, and related communities. """ import sys import subprocess import re import json from pathlib import Path from urllib.parse import quote from concurrent.futures import ThreadPoolExecutor, as_completed # Module metadata DESCRIPTION = "Adult site username OSINT scanner" AUTHOR = "darkHal" VERSION = "1.3" CATEGORY = "osint" sys.path.insert(0, str(Path(__file__).parent.parent)) from core.banner import Colors, clear_screen, display_banner from core.config import get_config # Custom sites storage file from core.paths import get_app_dir as _app_dir CUSTOM_SITES_FILE = _app_dir() / "custom_adultsites.json" # Bulk import file BULK_IMPORT_FILE = _app_dir() / "custom_sites.inf" # Common username URL patterns for auto-detection COMMON_PATTERNS = [ '/user/{}', '/users/{}', '/u/{}', '/profile/{}', '/profiles/{}', '/member/{}', '/members/{}', '/@{}', '/{}', '/people/{}', '/account/{}', '/id/{}', '/{}/profile', '/user/{}/profile', '/channel/{}', '/c/{}', '/p/{}', ] class AdultScanner: """Username scanner for adult-oriented sites.""" # Default site definitions: (name, url_template, method) # method: 'status' = check HTTP status, 'content' = check page content DEFAULT_SITES = { # Fanfiction & Story Sites 'fanfiction': [ ('Archive of Our Own', 'https://archiveofourown.org/users/{}/profile', 'status'), ('FanFiction.net', 'https://www.fanfiction.net/u/0/{}', 'content'), ('FimFiction', 'https://www.fimfiction.net/user/{}', 'status'), ('Wattpad', 'https://www.wattpad.com/user/{}', 'status'), ('Literotica', 'https://www.literotica.com/stories/memberpage.php?uid=0&username={}', 'content'), ('Adult-FanFiction', 'http://members.adult-fanfiction.org/profile.php?no=0&uname={}', 'content'), ('Hentai Foundry', 'https://www.hentai-foundry.com/user/{}/profile', 'status'), ('SoFurry', 'https://www.sofurry.com/browse/user/{}', 'status'), ('Inkbunny', 'https://inkbunny.net/{}', 'status'), ], # Art & Creative 'art': [ ('DeviantArt', 'https://www.deviantart.com/{}', 'status'), ('Fur Affinity', 'https://www.furaffinity.net/user/{}/', 'status'), ('Newgrounds', 'https://{}.newgrounds.com', 'status'), ('Pixiv', 'https://www.pixiv.net/en/users/{}', 'content'), ('Rule34', 'https://rule34.xxx/index.php?page=account&s=profile&uname={}', 'content'), ('e621', 'https://e621.net/users?name={}', 'content'), ('Derpibooru', 'https://derpibooru.org/profiles/{}', 'status'), ('Twitter/X', 'https://twitter.com/{}', 'status'), ('Tumblr', 'https://{}.tumblr.com', 'status'), ('Pillowfort', 'https://www.pillowfort.social/{}', 'status'), ], # Video & Streaming 'video': [ ('Pornhub', 'https://www.pornhub.com/users/{}', 'status'), ('XVideos', 'https://www.xvideos.com/profiles/{}', 'status'), ('xHamster', 'https://xhamster.com/users/{}', 'status'), ('Chaturbate', 'https://chaturbate.com/{}/', 'status'), ('OnlyFans', 'https://onlyfans.com/{}', 'status'), ('Fansly', 'https://fansly.com/{}', 'status'), ('ManyVids', 'https://www.manyvids.com/Profile/0/{}/', 'content'), ('PocketStars', 'https://pocketstars.com/{}', 'status'), ], # Forums & Communities 'forums': [ ('Reddit', 'https://www.reddit.com/user/{}', 'status'), ('F-List', 'https://www.f-list.net/c/{}', 'status'), ('FetLife', 'https://fetlife.com/users/{}', 'content'), ('Kink.com', 'https://www.kink.com/model/{}', 'content'), ('BDSMLR', 'https://{}.bdsmlr.com', 'status'), ('CollarSpace', 'https://www.collarspace.com/view/{}', 'content'), ], # Dating & Social 'dating': [ ('AdultFriendFinder', 'https://adultfriendfinder.com/p/{}', 'content'), ('Ashley Madison', 'https://www.ashleymadison.com/{}', 'content'), ('Grindr', 'https://www.grindr.com/{}', 'content'), ('Scruff', 'https://www.scruff.com/{}', 'content'), ('Recon', 'https://www.recon.com/{}', 'content'), ], # Gaming Related (with adult content) 'gaming': [ ('F95zone', 'https://f95zone.to/members/?username={}', 'content'), ('LoversLab', 'https://www.loverslab.com/profile/?name={}', 'content'), ('ULMF', 'https://ulmf.org/member.php?username={}', 'content'), ('Nutaku', 'https://www.nutaku.net/user/{}/', 'content'), ], } def __init__(self): self.results = [] self.config = get_config() osint_settings = self.config.get_osint_settings() self.timeout = osint_settings['timeout'] self.max_threads = osint_settings['max_threads'] # Copy default sites and add custom sites self.sites = {k: list(v) for k, v in self.DEFAULT_SITES.items()} self.sites['custom'] = [] self.load_custom_sites() def load_custom_sites(self): """Load custom sites from JSON file.""" if CUSTOM_SITES_FILE.exists(): try: with open(CUSTOM_SITES_FILE, 'r') as f: data = json.load(f) self.sites['custom'] = [tuple(site) for site in data.get('sites', [])] except Exception as e: self.sites['custom'] = [] def save_custom_sites(self): """Save custom sites to JSON file.""" try: data = {'sites': [list(site) for site in self.sites['custom']]} with open(CUSTOM_SITES_FILE, 'w') as f: json.dump(data, f, indent=2) return True except Exception as e: return False def add_custom_site(self): """Interactively add a custom site.""" print(f"\n{Colors.BOLD}Add Custom Site{Colors.RESET}") print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}") print() print(f"{Colors.CYAN}URL Pattern Format:{Colors.RESET}") print(f" Use {Colors.YELLOW}*{Colors.RESET} where the username should go") print(f" Example: {Colors.DIM}https://example.com/user/*{Colors.RESET}") print(f" Example: {Colors.DIM}https://example.com/profile?name=*{Colors.RESET}") print() # Get site name name = input(f"{Colors.WHITE}Site name: {Colors.RESET}").strip() if not name: self.print_status("Cancelled - no name provided", "warning") return # Get URL pattern url_pattern = input(f"{Colors.WHITE}URL pattern (use * for username): {Colors.RESET}").strip() if not url_pattern: self.print_status("Cancelled - no URL provided", "warning") return if '*' not in url_pattern: self.print_status("URL must contain * for username placeholder", "error") return # Convert * to {} for internal format url_template = url_pattern.replace('*', '{}') # Ensure URL has protocol if not url_template.startswith('http://') and not url_template.startswith('https://'): url_template = 'https://' + url_template # Get detection method print() print(f"{Colors.CYAN}Detection Method:{Colors.RESET}") print(f" {Colors.GREEN}[1]{Colors.RESET} Status code (default) - Check HTTP response code") print(f" {Colors.GREEN}[2]{Colors.RESET} Content - For sites with custom 404 pages") method_choice = input(f"{Colors.WHITE}Select [1]: {Colors.RESET}").strip() or "1" method = 'content' if method_choice == '2' else 'status' # Add to custom sites new_site = (name, url_template, method) self.sites['custom'].append(new_site) # Save to file if self.save_custom_sites(): self.print_status(f"Added '{name}' to custom sites", "success") print(f"{Colors.DIM} URL: {url_template.replace('{}', '')}{Colors.RESET}") else: self.print_status("Failed to save custom sites", "error") def manage_custom_sites(self): """View and manage custom sites.""" while True: clear_screen() display_banner() print(f"{Colors.BOLD}Manage Custom Sites{Colors.RESET}") print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}") print() custom = self.sites.get('custom', []) if not custom: print(f"{Colors.YELLOW}No custom sites added yet.{Colors.RESET}") print() print(f" {Colors.GREEN}[1]{Colors.RESET} Add New Site") print(f" {Colors.DIM}[0]{Colors.RESET} Back") print() choice = input(f"{Colors.WHITE}Select: {Colors.RESET}").strip() if choice == "1": self.add_custom_site() else: break else: print(f"{Colors.CYAN}Custom Sites ({len(custom)}):{Colors.RESET}") print() for i, (name, url, method) in enumerate(custom, 1): display_url = url.replace('{}', '*') method_tag = f"[{method}]" print(f" {Colors.GREEN}[{i}]{Colors.RESET} {name:25} {Colors.DIM}{method_tag}{Colors.RESET}") print(f" {Colors.DIM}{display_url}{Colors.RESET}") print() print(f" {Colors.GREEN}[A]{Colors.RESET} Add New Site") print(f" {Colors.RED}[R]{Colors.RESET} Remove Site") print(f" {Colors.DIM}[0]{Colors.RESET} Back") print() choice = input(f"{Colors.WHITE}Select: {Colors.RESET}").strip().upper() if choice == "0": break elif choice == "A": self.add_custom_site() elif choice == "R": self.remove_custom_site() def remove_custom_site(self): """Remove a custom site.""" custom = self.sites.get('custom', []) if not custom: self.print_status("No custom sites to remove", "warning") return print() idx_input = input(f"{Colors.WHITE}Enter site number to remove: {Colors.RESET}").strip() try: idx = int(idx_input) - 1 if 0 <= idx < len(custom): removed = custom.pop(idx) if self.save_custom_sites(): self.print_status(f"Removed '{removed[0]}'", "success") else: self.print_status("Failed to save changes", "error") else: self.print_status("Invalid selection", "error") except ValueError: self.print_status("Invalid number", "error") input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}") def auto_detect_site(self): """Auto-detect URL pattern for a domain.""" print(f"\n{Colors.BOLD}Auto-Detect Site Pattern{Colors.RESET}") print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}") print() print(f"{Colors.CYAN}Enter just the domain name and we'll find the pattern.{Colors.RESET}") print(f"{Colors.DIM}Example: example.com or www.example.com{Colors.RESET}") print() # Get domain domain = input(f"{Colors.WHITE}Domain: {Colors.RESET}").strip() if not domain: self.print_status("Cancelled - no domain provided", "warning") return # Clean up domain domain = domain.replace('https://', '').replace('http://', '').rstrip('/') # Get test username print() print(f"{Colors.CYAN}We need a known username to test patterns.{Colors.RESET}") print(f"{Colors.DIM}Enter a username that you know EXISTS on this site.{Colors.RESET}") test_user = input(f"{Colors.WHITE}Test username: {Colors.RESET}").strip() if not test_user: self.print_status("Cancelled - no test username provided", "warning") return print(f"\n{Colors.CYAN}Testing {len(COMMON_PATTERNS)} common URL patterns...{Colors.RESET}\n") # Test each pattern working_patterns = [] for i, pattern in enumerate(COMMON_PATTERNS): url = f"https://{domain}{pattern}".format(test_user) print(f"\r{Colors.DIM} Testing pattern {i+1}/{len(COMMON_PATTERNS)}: {pattern}{' ' * 20}{Colors.RESET}", end="") cmd = f"curl -sI -o /dev/null -w '%{{http_code}}' -L --max-time 5 '{url}' 2>/dev/null" success, output, _ = self.run_cmd(cmd, 7) if success: status_code = output.strip() if status_code in ['200', '301', '302']: working_patterns.append((pattern, status_code, url)) print(f"\r{' ' * 80}\r", end="") # Clear line if not working_patterns: print(f"{Colors.YELLOW}No working patterns found.{Colors.RESET}") print(f"{Colors.DIM}The site may use a non-standard URL format.{Colors.RESET}") print(f"{Colors.DIM}Try using manual add [A] with the correct URL pattern.{Colors.RESET}") return # Display working patterns print(f"{Colors.GREEN}Found {len(working_patterns)} working pattern(s):{Colors.RESET}\n") for i, (pattern, status, url) in enumerate(working_patterns, 1): status_info = "OK" if status == '200' else f"redirect ({status})" print(f" {Colors.GREEN}[{i}]{Colors.RESET} {pattern:20} {Colors.DIM}({status_info}){Colors.RESET}") print(f" {Colors.DIM}{url}{Colors.RESET}") print() # Let user select print(f" {Colors.DIM}[0]{Colors.RESET} Cancel") print() choice = input(f"{Colors.WHITE}Select pattern to add: {Colors.RESET}").strip() try: idx = int(choice) - 1 if 0 <= idx < len(working_patterns): selected_pattern, status, _ = working_patterns[idx] url_template = f"https://{domain}{selected_pattern}" # Get site name default_name = domain.split('.')[0].title() name = input(f"{Colors.WHITE}Site name [{default_name}]: {Colors.RESET}").strip() or default_name # Determine method based on status method = 'status' if status == '200' else 'content' # Add to custom sites new_site = (name, url_template, method) self.sites['custom'].append(new_site) if self.save_custom_sites(): self.print_status(f"Added '{name}' to custom sites", "success") print(f"{Colors.DIM} Pattern: {url_template.replace('{}', '*')}{Colors.RESET}") else: self.print_status("Failed to save custom sites", "error") elif choice != "0": self.print_status("Cancelled", "warning") except ValueError: if choice != "0": self.print_status("Invalid selection", "error") def probe_domain(self, domain: str, test_user: str, quiet: bool = False) -> list: """Probe a domain for working URL patterns. Returns list of (pattern, status_code, url).""" domain = domain.replace('https://', '').replace('http://', '').rstrip('/') working_patterns = [] for i, pattern in enumerate(COMMON_PATTERNS): url = f"https://{domain}{pattern}".format(test_user) if not quiet: print(f"\r{Colors.DIM} Testing {domain}: pattern {i+1}/{len(COMMON_PATTERNS)}{' ' * 20}{Colors.RESET}", end="") cmd = f"curl -sI -o /dev/null -w '%{{http_code}}' -L --max-time 5 '{url}' 2>/dev/null" success, output, _ = self.run_cmd(cmd, 7) if success: status_code = output.strip() if status_code in ['200', '301', '302']: working_patterns.append((pattern, status_code, url)) # For bulk mode, take first working pattern and stop if quiet: break if not quiet: print(f"\r{' ' * 80}\r", end="") return working_patterns def bulk_import(self): """Bulk import sites from custom_sites.inf file.""" print(f"\n{Colors.BOLD}Bulk Import Sites{Colors.RESET}") print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}") print() # Check if file exists, create template if not if not BULK_IMPORT_FILE.exists(): print(f"{Colors.YELLOW}Bulk import file not found.{Colors.RESET}") print(f"{Colors.DIM}Creating template at: {BULK_IMPORT_FILE}{Colors.RESET}") print() create = input(f"{Colors.WHITE}Create template file? (y/n): {Colors.RESET}").strip().lower() if create == 'y': template = """# AUTARCH Adult Site Scanner - Bulk Import File # Add one domain per line (without http:// or https://) # Lines starting with # are comments # # Example: # example.com # another-site.net # subdomain.site.org # # After adding domains, run Bulk Import [B] again # and provide a test username that exists on these sites. """ with open(BULK_IMPORT_FILE, 'w') as f: f.write(template) self.print_status(f"Created {BULK_IMPORT_FILE}", "success") print(f"{Colors.DIM}Edit this file and add domains, then run Bulk Import again.{Colors.RESET}") return # Read domains from file domains = [] with open(BULK_IMPORT_FILE, 'r') as f: for line in f: line = line.strip() # Skip empty lines and comments if line and not line.startswith('#'): # Clean up domain domain = line.replace('https://', '').replace('http://', '').rstrip('/') if domain: domains.append(domain) if not domains: print(f"{Colors.YELLOW}No domains found in {BULK_IMPORT_FILE.name}{Colors.RESET}") print(f"{Colors.DIM}Add domains (one per line) and try again.{Colors.RESET}") return print(f"{Colors.CYAN}Found {len(domains)} domain(s) in {BULK_IMPORT_FILE.name}:{Colors.RESET}") for d in domains[:10]: print(f" {Colors.DIM}-{Colors.RESET} {d}") if len(domains) > 10: print(f" {Colors.DIM}... and {len(domains) - 10} more{Colors.RESET}") print() # Check which domains are already added existing_domains = set() for name, url, method in self.sites.get('custom', []): # Extract domain from URL template try: from urllib.parse import urlparse parsed = urlparse(url.replace('{}', 'test')) existing_domains.add(parsed.netloc.lower()) except: pass new_domains = [d for d in domains if d.lower() not in existing_domains] skipped = len(domains) - len(new_domains) if skipped > 0: print(f"{Colors.YELLOW}Skipping {skipped} already-added domain(s){Colors.RESET}") if not new_domains: print(f"{Colors.GREEN}All domains already added!{Colors.RESET}") return print(f"{Colors.CYAN}Will scan {len(new_domains)} new domain(s){Colors.RESET}") print() # Get test username print(f"{Colors.CYAN}We need a test username to probe URL patterns.{Colors.RESET}") print(f"{Colors.DIM}Use a common username that likely exists on most sites.{Colors.RESET}") print(f"{Colors.DIM}Example: admin, test, user, john, etc.{Colors.RESET}") print() test_user = input(f"{Colors.WHITE}Test username: {Colors.RESET}").strip() if not test_user: self.print_status("Cancelled - no test username provided", "warning") return print(f"\n{Colors.CYAN}Scanning {len(new_domains)} domains...{Colors.RESET}\n") # Scan each domain added = 0 failed = [] for i, domain in enumerate(new_domains): print(f"{Colors.DIM}[{i+1}/{len(new_domains)}] Scanning {domain}...{Colors.RESET}") # Use quiet mode to get first working pattern patterns = self.probe_domain(domain, test_user, quiet=True) if patterns: pattern, status, url = patterns[0] url_template = f"https://{domain}{pattern}" name = domain.split('.')[0].title() method = 'status' if status == '200' else 'content' # Add to custom sites new_site = (name, url_template, method) self.sites['custom'].append(new_site) added += 1 print(f" {Colors.GREEN}[+]{Colors.RESET} Added {name}: {pattern}") else: failed.append(domain) print(f" {Colors.RED}[X]{Colors.RESET} No pattern found") # Save results if added > 0: if self.save_custom_sites(): print(f"\n{Colors.GREEN}Successfully added {added} site(s){Colors.RESET}") else: print(f"\n{Colors.RED}Failed to save custom sites{Colors.RESET}") if failed: print(f"\n{Colors.YELLOW}Failed to detect patterns for {len(failed)} domain(s):{Colors.RESET}") for d in failed[:5]: print(f" {Colors.DIM}-{Colors.RESET} {d}") if len(failed) > 5: print(f" {Colors.DIM}... and {len(failed) - 5} more{Colors.RESET}") print(f"{Colors.DIM}Try adding these manually with [A] or [D]{Colors.RESET}") # Offer to clear the import file print() clear_file = input(f"{Colors.WHITE}Clear import file? (y/n): {Colors.RESET}").strip().lower() if clear_file == 'y': # Keep the header comments header = """# AUTARCH Adult Site Scanner - Bulk Import File # Add one domain per line (without http:// or https://) # Lines starting with # are comments """ with open(BULK_IMPORT_FILE, 'w') as f: f.write(header) self.print_status("Import file cleared", "success") def print_status(self, message: str, status: str = "info"): colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED} symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"} print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}") def run_cmd(self, cmd: str, timeout: int = 10) -> tuple: try: result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) return result.returncode == 0, result.stdout.strip(), result.stderr.strip() except subprocess.TimeoutExpired: return False, "", "timeout" except Exception as e: return False, "", str(e) def check_site(self, site_info: tuple, username: str) -> dict: """Check if username exists on a site.""" name, url_template, method = site_info # Handle special URL formats if '{}' in url_template: url = url_template.format(username) else: url = url_template + username result = { 'site': name, 'url': url, 'found': False, 'status': 'unknown' } # Use curl to check cmd = f"curl -sI -o /dev/null -w '%{{http_code}}' -L --max-time {self.timeout} '{url}' 2>/dev/null" success, output, _ = self.run_cmd(cmd, self.timeout + 2) if success: status_code = output.strip() if method == 'status': # Check HTTP status code if status_code == '200': result['found'] = True result['status'] = 'found' elif status_code in ['301', '302']: result['found'] = True result['status'] = 'redirect' elif status_code == '404': result['status'] = 'not_found' else: result['status'] = f'http_{status_code}' else: # For content-based checks, we need to fetch the page if status_code == '200': # Could do content analysis here result['found'] = True result['status'] = 'possible' elif status_code == '404': result['status'] = 'not_found' else: result['status'] = f'http_{status_code}' else: result['status'] = 'error' return result def scan_username(self, username: str, categories: list = None): """Scan username across selected site categories.""" if categories is None: categories = list(self.sites.keys()) # Collect all sites to scan sites_to_scan = [] for cat in categories: if cat in self.sites: sites_to_scan.extend(self.sites[cat]) print(f"\n{Colors.CYAN}Scanning {len(sites_to_scan)} sites for username: {username}{Colors.RESET}") print(f"{Colors.DIM}This may take a few minutes...{Colors.RESET}\n") self.results = [] found_count = 0 # Use thread pool for parallel scanning with ThreadPoolExecutor(max_workers=self.max_threads) as executor: futures = {executor.submit(self.check_site, site, username): site for site in sites_to_scan} for i, future in enumerate(as_completed(futures)): result = future.result() self.results.append(result) # Display progress if result['found']: found_count += 1 status_color = Colors.GREEN if result['status'] == 'found' else Colors.YELLOW print(f" {status_color}[+]{Colors.RESET} {result['site']:25} {result['url']}") else: # Show progress indicator print(f"\r{Colors.DIM} Checked {i+1}/{len(sites_to_scan)} sites, found {found_count}...{Colors.RESET}", end="") print(f"\r{' ' * 60}\r", end="") # Clear progress line return self.results def display_results(self): """Display scan results.""" found = [r for r in self.results if r['found']] not_found = [r for r in self.results if not r['found']] print(f"\n{Colors.BOLD}{'─' * 60}{Colors.RESET}") print(f"{Colors.BOLD}Scan Results{Colors.RESET}") print(f"{Colors.BOLD}{'─' * 60}{Colors.RESET}\n") if found: print(f"{Colors.GREEN}Found ({len(found)} sites):{Colors.RESET}\n") for r in found: status_note = f" ({r['status']})" if r['status'] not in ['found'] else "" print(f" {Colors.GREEN}+{Colors.RESET} {r['site']:25} {r['url']}{Colors.DIM}{status_note}{Colors.RESET}") else: print(f"{Colors.YELLOW}No profiles found.{Colors.RESET}") print(f"\n{Colors.DIM}Total sites checked: {len(self.results)}{Colors.RESET}") print(f"{Colors.DIM}Profiles found: {len(found)}{Colors.RESET}") def export_results(self, filename: str): """Export results to file.""" found = [r for r in self.results if r['found']] with open(filename, 'w') as f: f.write(f"Username OSINT Results\n") f.write(f"{'=' * 50}\n\n") f.write(f"Found Profiles ({len(found)}):\n\n") for r in found: f.write(f"{r['site']}: {r['url']}\n") self.print_status(f"Results exported to {filename}", "success") def show_menu(self): """Display main menu.""" clear_screen() display_banner() print(f"{Colors.GREEN}{Colors.BOLD} Adult Site Scanner{Colors.RESET}") print(f"{Colors.DIM} Username OSINT for adult platforms{Colors.RESET}") print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}") print() # Show category counts total = sum(len(sites) for sites in self.sites.values()) custom_count = len(self.sites.get('custom', [])) print(f"{Colors.DIM} Sites in database: {total} ({custom_count} custom){Colors.RESET}") print() print(f" {Colors.CYAN}Scan Categories:{Colors.RESET}") print(f" {Colors.GREEN}[1]{Colors.RESET} Full Scan (all categories)") print(f" {Colors.GREEN}[2]{Colors.RESET} Fanfiction & Story Sites") print(f" {Colors.GREEN}[3]{Colors.RESET} Art & Creative Sites") print(f" {Colors.GREEN}[4]{Colors.RESET} Video & Streaming Sites") print(f" {Colors.GREEN}[5]{Colors.RESET} Forums & Communities") print(f" {Colors.GREEN}[6]{Colors.RESET} Dating & Social Sites") print(f" {Colors.GREEN}[7]{Colors.RESET} Gaming Related Sites") print(f" {Colors.GREEN}[8]{Colors.RESET} Custom Sites Only") print(f" {Colors.GREEN}[9]{Colors.RESET} Custom Category Selection") print() print(f" {Colors.CYAN}Site Management:{Colors.RESET}") print(f" {Colors.GREEN}[A]{Colors.RESET} Add Custom Site (manual)") print(f" {Colors.GREEN}[D]{Colors.RESET} Auto-Detect Site Pattern") print(f" {Colors.GREEN}[B]{Colors.RESET} Bulk Import from File") print(f" {Colors.GREEN}[M]{Colors.RESET} Manage Custom Sites") print(f" {Colors.GREEN}[L]{Colors.RESET} List All Sites") print() print(f" {Colors.DIM}[0]{Colors.RESET} Back") print() def select_categories(self) -> list: """Let user select multiple categories.""" print(f"\n{Colors.BOLD}Select Categories (comma-separated):{Colors.RESET}") print() cat_list = list(self.sites.keys()) for i, cat in enumerate(cat_list, 1): count = len(self.sites[cat]) print(f" [{i}] {cat.title():20} ({count} sites)") print() selection = input(f"{Colors.WHITE}Enter numbers (e.g., 1,2,3): {Colors.RESET}").strip() selected = [] try: for num in selection.split(','): idx = int(num.strip()) - 1 if 0 <= idx < len(cat_list): selected.append(cat_list[idx]) except: pass return selected if selected else None def list_sites(self): """List all sites in database.""" clear_screen() display_banner() print(f"{Colors.BOLD}Site Database{Colors.RESET}") print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n") for category, sites in self.sites.items(): if not sites: continue color = Colors.YELLOW if category == 'custom' else Colors.GREEN print(f"{color}{category.upper()} ({len(sites)} sites){Colors.RESET}") for name, url, method in sites: if category == 'custom': display_url = url.replace('{}', '*') print(f" {Colors.DIM}-{Colors.RESET} {name} {Colors.DIM}({display_url}){Colors.RESET}") else: print(f" {Colors.DIM}-{Colors.RESET} {name}") print() input(f"{Colors.WHITE}Press Enter to continue...{Colors.RESET}") def run_scan(self, categories: list = None): """Run a scan with given categories.""" username = input(f"\n{Colors.WHITE}Enter username to search: {Colors.RESET}").strip() if not username: return self.scan_username(username, categories) self.display_results() # Export option export = input(f"\n{Colors.WHITE}Export results to file? (y/n): {Colors.RESET}").strip().lower() if export == 'y': filename = f"{username}_adultscan.txt" self.export_results(filename) def run(self): """Main loop.""" while True: self.show_menu() try: choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().upper() if choice == "0": break elif choice == "1": self.run_scan() # All categories elif choice == "2": self.run_scan(['fanfiction']) elif choice == "3": self.run_scan(['art']) elif choice == "4": self.run_scan(['video']) elif choice == "5": self.run_scan(['forums']) elif choice == "6": self.run_scan(['dating']) elif choice == "7": self.run_scan(['gaming']) elif choice == "8": if self.sites.get('custom'): self.run_scan(['custom']) else: self.print_status("No custom sites added yet. Use [A] to add sites.", "warning") input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}") continue elif choice == "9": cats = self.select_categories() if cats: self.run_scan(cats) elif choice == "A": self.add_custom_site() input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}") continue elif choice == "D": self.auto_detect_site() input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}") continue elif choice == "B": self.bulk_import() input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}") continue elif choice == "M": self.manage_custom_sites() continue elif choice == "L": self.list_sites() continue if choice in ["1", "2", "3", "4", "5", "6", "7", "8", "9"]: input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}") except (EOFError, KeyboardInterrupt): break def run(): AdultScanner().run() if __name__ == "__main__": run()