Spaces:
Paused
Paused
| """ | |
| RentMasseur Traffic Engine — persistent daemon that: | |
| 1. Maintains authenticated session (re-login on expiry) | |
| 2. Auto-refreshes availability before expiry (default: every 55 min) | |
| 3. Tracks profile stats (views, contacts, online bookmarks) | |
| 4. Logs search position for target city | |
| 5. Persists everything to SQLite for trend analysis | |
| 6. Sends alerts on status changes (visibility, membership, ranking drops) | |
| Usage: | |
| python -m rm_traffic.engine # run daemon | |
| python -m rm_traffic.engine --once # single cycle | |
| python -m rm_traffic.engine --stats # print stats summary | |
| python -m rm_traffic.engine --watch "Manhattan, NY" # track search position | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import sqlite3 | |
| import subprocess | |
| import sys | |
| import time | |
| import logging | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| # Selenium | |
| from selenium import webdriver | |
| from selenium.webdriver.chrome.options import Options | |
| from selenium.webdriver.common.by import By | |
| from selenium.common.exceptions import ( | |
| StaleElementReferenceException, | |
| NoSuchElementException, | |
| TimeoutException, | |
| WebDriverException, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Config | |
| # --------------------------------------------------------------------------- | |
| USERNAME = "karpathianwolf" | |
| PASSWORD = os.environ.get("RM_PASSWORD", "") | |
| BASE_URL = "https://rentmasseur.com" | |
| DB_PATH = Path(__file__).parent / "traffic.db" | |
| LOG_PATH = Path(__file__).parent / "traffic.log" | |
| CHROME_PROFILE = "/tmp/rm_traffic_chrome" | |
| REFRESH_INTERVAL = 55 * 60 # 55 minutes — refresh before 1h expiry | |
| STAT_INTERVAL = 5 * 60 # check stats every 5 min | |
| SEARCH_CHECK_INTERVAL = 30 * 60 # check search position every 30 min | |
| # --------------------------------------------------------------------------- | |
| # Logging | |
| # --------------------------------------------------------------------------- | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(message)s", | |
| handlers=[ | |
| logging.FileHandler(LOG_PATH), | |
| logging.StreamHandler(sys.stdout), | |
| ], | |
| ) | |
| log = logging.getLogger("rm_traffic") | |
| # --------------------------------------------------------------------------- | |
| # Database | |
| # --------------------------------------------------------------------------- | |
| def db(): | |
| conn = sqlite3.connect(str(DB_PATH)) | |
| conn.row_factory = sqlite3.Row | |
| conn.execute("PRAGMA journal_mode=WAL") | |
| return conn | |
| def init_db(): | |
| conn = db() | |
| conn.executescript(""" | |
| CREATE TABLE IF NOT EXISTS availability_log ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| ts TEXT NOT NULL, | |
| action TEXT NOT NULL, | |
| status TEXT, | |
| hours INTEGER, | |
| minutes INTEGER, | |
| duration_set INTEGER, | |
| success INTEGER, | |
| detail TEXT | |
| ); | |
| CREATE TABLE IF NOT EXISTS stats_log ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| ts TEXT NOT NULL, | |
| profile_views INTEGER, | |
| contact_clicks INTEGER, | |
| online_bookmarks INTEGER, | |
| who_saw_me INTEGER, | |
| membership_status TEXT, | |
| visibility TEXT, | |
| availability_status TEXT, | |
| availability_remaining TEXT | |
| ); | |
| CREATE TABLE IF NOT EXISTS search_position ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| ts TEXT NOT NULL, | |
| city TEXT NOT NULL, | |
| position INTEGER, | |
| total_results INTEGER, | |
| available_now INTEGER, | |
| username TEXT | |
| ); | |
| CREATE TABLE IF NOT EXISTS alerts ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| ts TEXT NOT NULL, | |
| type TEXT NOT NULL, | |
| severity TEXT, | |
| message TEXT, | |
| acknowledged INTEGER DEFAULT 0 | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_avail_ts ON availability_log(ts); | |
| CREATE INDEX IF NOT EXISTS idx_stats_ts ON stats_log(ts); | |
| CREATE INDEX IF NOT EXISTS idx_search_ts ON search_position(ts); | |
| """) | |
| conn.commit() | |
| conn.close() | |
| log.info("DB initialized at %s", DB_PATH) | |
| def log_availability(action, status=None, hours=None, minutes=None, | |
| duration_set=None, success=True, detail=""): | |
| conn = db() | |
| conn.execute( | |
| "INSERT INTO availability_log (ts, action, status, hours, minutes, duration_set, success, detail) " | |
| "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", | |
| (datetime.now(timezone.utc).isoformat(), action, status, hours, minutes, | |
| duration_set, int(success), detail) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def log_stats(views, contacts, bookmarks, who_saw, membership, visibility, | |
| avail_status, avail_remaining): | |
| conn = db() | |
| conn.execute( | |
| "INSERT INTO stats_log (ts, profile_views, contact_clicks, online_bookmarks, " | |
| "who_saw_me, membership_status, visibility, availability_status, availability_remaining) " | |
| "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", | |
| (datetime.now(timezone.utc).isoformat(), views, contacts, bookmarks, | |
| who_saw, membership, visibility, avail_status, avail_remaining) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def log_search_position(city, position, total, available_now, username): | |
| conn = db() | |
| conn.execute( | |
| "INSERT INTO search_position (ts, city, position, total_results, available_now, username) " | |
| "VALUES (?, ?, ?, ?, ?, ?)", | |
| (datetime.now(timezone.utc).isoformat(), city, position, total, available_now, username) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def log_alert(atype, severity, message): | |
| conn = db() | |
| conn.execute( | |
| "INSERT INTO alerts (ts, type, severity, message) VALUES (?, ?, ?, ?)", | |
| (datetime.now(timezone.utc).isoformat(), atype, severity, message) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| log.warning("ALERT [%s] %s: %s", severity, atype, message) | |
| # --------------------------------------------------------------------------- | |
| # Browser | |
| # --------------------------------------------------------------------------- | |
| class Browser: | |
| """Managed Chrome instance with session persistence.""" | |
| def __init__(self): | |
| self.driver = None | |
| self.logged_in = False | |
| self.last_login = 0 | |
| def start(self): | |
| opts = Options() | |
| opts.binary_location = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" | |
| opts.add_argument("--no-sandbox") | |
| opts.add_argument("--disable-dev-shm-usage") | |
| opts.add_argument("--window-size=1920,1080") | |
| opts.add_argument(f"--user-data-dir={CHROME_PROFILE}") | |
| opts.add_argument("--disable-blink-features=AutomationControlled") | |
| opts.add_experimental_option("excludeSwitches", ["enable-automation"]) | |
| opts.add_experimental_option("useAutomationExtension", False) | |
| self.driver = webdriver.Chrome(options=opts) | |
| # Remove webdriver flag | |
| self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { | |
| "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" | |
| }) | |
| log.info("Browser started (profile: %s)", CHROME_PROFILE) | |
| def stop(self): | |
| if self.driver: | |
| try: | |
| self.driver.quit() | |
| except Exception: | |
| pass | |
| self.driver = None | |
| self.logged_in = False | |
| log.info("Browser stopped") | |
| def _dismiss_popups(self): | |
| time.sleep(1) | |
| for xpath in [ | |
| "//button[contains(text(),'Not now')]", | |
| "//button[contains(text(),'Accept all')]", | |
| "//button[contains(text(),'Close')]", | |
| "//*[contains(@aria-label,'Close')]", | |
| ]: | |
| try: | |
| for el in self.driver.find_elements(By.XPATH, xpath): | |
| if el.is_displayed(): | |
| el.click() | |
| time.sleep(0.5) | |
| except Exception: | |
| pass | |
| def login(self, force=False): | |
| if self.logged_in and not force and (time.time() - self.last_login < 3600): | |
| return True | |
| if not self.driver: | |
| self.start() | |
| log.info("Logging in as %s...", USERNAME) | |
| self.driver.get(f"{BASE_URL}/login") | |
| time.sleep(4) | |
| self._dismiss_popups() | |
| # Find login fields | |
| email = None | |
| pwd = None | |
| for inp in self.driver.find_elements(By.CSS_SELECTOR, "input"): | |
| itype = (inp.get_attribute("type") or "").lower() | |
| iname = (inp.get_attribute("name") or "").lower() | |
| if itype == "email" or "email" in iname or "user" in iname: | |
| if inp.is_displayed(): | |
| email = inp | |
| if itype == "password" and inp.is_displayed(): | |
| pwd = inp | |
| if not email or not pwd: | |
| log.error("Login fields not found") | |
| log_availability("login", success=False, detail="fields not found") | |
| return False | |
| email.clear() | |
| email.send_keys(USERNAME) | |
| pwd.clear() | |
| pwd.send_keys(PASSWORD) | |
| time.sleep(0.3) | |
| # Click login button | |
| clicked = False | |
| for by, sel in [ | |
| (By.CSS_SELECTOR, "button[type='submit']"), | |
| (By.XPATH, "//button[contains(text(),'LOG')]"), | |
| (By.XPATH, "//button[contains(text(),'Log')]"), | |
| (By.CSS_SELECTOR, "form button"), | |
| ]: | |
| for el in self.driver.find_elements(by, sel): | |
| if el.is_displayed(): | |
| el.click() | |
| clicked = True | |
| break | |
| if clicked: | |
| break | |
| if not clicked: | |
| log.error("Login button not found") | |
| return False | |
| time.sleep(6) | |
| self._dismiss_popups() | |
| # Verify login | |
| url = self.driver.current_url | |
| if "/login" in url: | |
| log.error("Login failed — still on login page") | |
| log_availability("login", success=False, detail="redirected back to login") | |
| return False | |
| self.logged_in = True | |
| self.last_login = time.time() | |
| log.info("Login successful. URL: %s", url) | |
| log_availability("login", success=True, detail=url) | |
| return True | |
| def ensure_session(self): | |
| """Check if session is still valid, re-login if not.""" | |
| if not self.driver: | |
| return self.login() | |
| try: | |
| self.driver.get(f"{BASE_URL}/settings") | |
| time.sleep(3) | |
| if "/login" in self.driver.current_url: | |
| log.info("Session expired, re-logging in...") | |
| return self.login(force=True) | |
| return True | |
| except WebDriverException as e: | |
| log.warning("Session check failed: %s", e) | |
| return self.login(force=True) | |
| def get_settings_data(self): | |
| """Fetch settings page and extract all profile data.""" | |
| if not self.ensure_session(): | |
| return None | |
| self.driver.get(f"{BASE_URL}/settings") | |
| time.sleep(5) | |
| self._dismiss_popups() | |
| try: | |
| body = self.driver.find_element(By.TAG_NAME, "body").text | |
| except Exception: | |
| return None | |
| return body | |
| def set_available(self, duration=1): | |
| """Set availability to 'Available' with given duration (hours).""" | |
| if not self.ensure_session(): | |
| return False | |
| log.info("Setting availability: Available, %dh", duration) | |
| self.driver.get(f"{BASE_URL}/settings?availability=1") | |
| time.sleep(5) | |
| self._dismiss_popups() | |
| # Check current status first | |
| body = self.driver.find_element(By.TAG_NAME, "body").text | |
| match = re.search(r'Status:\s*Available\s*(\d+)h\s*:\s*(\d+)m', body) | |
| if match: | |
| hours = int(match.group(1)) | |
| mins = int(match.group(2)) | |
| total_mins = hours * 60 + mins | |
| if total_mins > 10: | |
| log.info("Already available (%dh:%dm remaining), skipping", hours, mins) | |
| log_availability("skip", status="Available", hours=hours, | |
| minutes=mins, success=True, detail="sufficient time remaining") | |
| return True | |
| # Click the availability panel to open modal | |
| try: | |
| avail_panel = self.driver.find_element(By.CSS_SELECTOR, ".AvailabilityPanel") | |
| avail_panel.click() | |
| time.sleep(2) | |
| except (NoSuchElementException, StaleElementReferenceException): | |
| log.warning("Could not click availability panel") | |
| log_availability("set", success=False, detail="panel not found") | |
| return False | |
| # Wait for modal and find "Available" option | |
| time.sleep(1) | |
| clicked_available = False | |
| for by, sel in [ | |
| (By.XPATH, "//label[contains(text(),'Available')]"), | |
| (By.XPATH, "//*[contains(text(),'Available') and not(contains(text(),'Not')) and not(contains(text(),'Status'))]"), | |
| (By.CSS_SELECTOR, "input[type='radio'][value='1']"), | |
| (By.CSS_SELECTOR, "input[type='radio']"), | |
| ]: | |
| try: | |
| els = self.driver.find_elements(by, sel) | |
| for el in els: | |
| if el.is_displayed(): | |
| el.click() | |
| clicked_available = True | |
| log.info("Selected 'Available'") | |
| break | |
| except Exception: | |
| pass | |
| if clicked_available: | |
| break | |
| if not clicked_available: | |
| log.warning("Could not select 'Available' option") | |
| log_availability("set", success=False, detail="could not select Available") | |
| return False | |
| # Select duration | |
| time.sleep(0.5) | |
| for sel_el in self.driver.find_elements(By.CSS_SELECTOR, "select"): | |
| try: | |
| from selenium.webdriver.support.ui import Select | |
| sel = Select(sel_el) | |
| options = [o.text for o in sel.options] | |
| target = f"{duration} Hour" if duration == 1 else f"{duration} Hours" | |
| if target in options: | |
| sel.select_by_visible_text(target) | |
| log.info("Selected duration: %s", target) | |
| break | |
| except Exception: | |
| pass | |
| # Click SET button | |
| time.sleep(0.5) | |
| set_clicked = False | |
| for by, sel in [ | |
| (By.XPATH, "//button[contains(text(),'SET')]"), | |
| (By.XPATH, "//button[contains(text(),'Set')]"), | |
| ]: | |
| try: | |
| els = self.driver.find_elements(by, sel) | |
| for el in els: | |
| if el.is_displayed(): | |
| el.click() | |
| set_clicked = True | |
| log.info("Clicked SET") | |
| break | |
| except Exception: | |
| pass | |
| if set_clicked: | |
| break | |
| if not set_clicked: | |
| log.warning("SET button not found") | |
| log_availability("set", success=False, detail="SET button not found") | |
| return False | |
| time.sleep(3) | |
| # Verify | |
| body = self.driver.find_element(By.TAG_NAME, "body").text | |
| if "Status: Available" in body: | |
| match = re.search(r'Status:\s*Available\s*(\d+)h\s*:\s*(\d+)m', body) | |
| if match: | |
| h, m = int(match.group(1)), int(match.group(2)) | |
| log.info("Availability confirmed: %dh:%dm", h, m) | |
| log_availability("set", status="Available", hours=h, minutes=m, | |
| duration_set=duration, success=True) | |
| return True | |
| log.warning("Availability set but could not verify") | |
| log_availability("set", success=True, detail="could not verify") | |
| return True | |
| def get_stats(self): | |
| """Extract profile statistics from settings page.""" | |
| body = self.get_settings_data() | |
| if not body: | |
| return None | |
| stats = { | |
| "profile_views": None, | |
| "contact_clicks": None, | |
| "online_bookmarks": None, | |
| "who_saw_me": None, | |
| "membership_status": None, | |
| "visibility": None, | |
| "availability_status": None, | |
| "availability_remaining": None, | |
| } | |
| # Profile Views: 77999 | |
| m = re.search(r'Profile Views:\s*([\d,]+)', body) | |
| if m: | |
| stats["profile_views"] = int(m.group(1).replace(",", "")) | |
| # Who Saw Me: 30 | |
| m = re.search(r'Who Saw Me\s*(\d+)', body) | |
| if m: | |
| stats["who_saw_me"] = int(m.group(1)) | |
| # Online bookmarks | |
| m = re.search(r'(\d+)\s*online', body) | |
| if m: | |
| stats["online_bookmarks"] = int(m.group(1)) | |
| # Membership | |
| m = re.search(r'Profile Status:\s*(\w+)', body) | |
| if m: | |
| stats["membership_status"] = m.group(1) | |
| # Visibility | |
| if "Profile hidden" in body: | |
| stats["visibility"] = "hidden" | |
| elif "Profile shown" in body: | |
| stats["visibility"] = "shown" | |
| # Availability | |
| m = re.search(r'Status:\s*(Available|Not Available|Not Set)\s*(\d+h\s*:\s*\d+m)?', body) | |
| if m: | |
| stats["availability_status"] = m.group(1) | |
| stats["availability_remaining"] = m.group(2) or "" | |
| # Contact clicks from ad statistics | |
| m = re.search(r'Contact Me\s*clicks\s*([\d,]+)', body) | |
| if m: | |
| stats["contact_clicks"] = int(m.group(1).replace(",", "")) | |
| return stats | |
| def check_search_position(self, city="Manhattan, NY"): | |
| """Check profile position in search results for a city.""" | |
| if not self.ensure_session(): | |
| return None | |
| city_slug = city.lower().replace(", ", "-").replace(" ", "-") | |
| url = f"{BASE_URL}/gay-massage/{city_slug}/" | |
| log.info("Checking search position at %s", url) | |
| self.driver.get(url) | |
| time.sleep(5) | |
| self._dismiss_popups() | |
| # Find all masseur profile links | |
| body = self.driver.find_element(By.TAG_NAME, "body").text | |
| # Find profile cards/links | |
| profile_links = [] | |
| for el in self.driver.find_elements(By.CSS_SELECTOR, "a[href^='/']"): | |
| href = el.get_attribute("href") or "" | |
| text = el.text.strip().lower() | |
| # Profile links are like /Username | |
| if href.startswith(f"{BASE_URL}/") and not any(x in href for x in [ | |
| "login", "settings", "search", "blog", "reviews", "interviews", | |
| "available", "find-massage", "live-cams", "sitemap", "advertise", | |
| "mailbox", "static", "_next", "images", | |
| ]): | |
| parts = href.replace(f"{BASE_URL}/", "").split("/") | |
| if parts and parts[0] and len(parts) == 1: | |
| profile_links.append((parts[0].lower(), href)) | |
| # Find our position | |
| username_lower = USERNAME.lower() | |
| position = None | |
| for i, (uname, href) in enumerate(profile_links): | |
| if uname == username_lower: | |
| position = i + 1 | |
| break | |
| # Check available now filter | |
| avail_url = f"{url}?available=1" | |
| self.driver.get(avail_url) | |
| time.sleep(4) | |
| self._dismiss_popups() | |
| avail_links = [] | |
| for el in self.driver.find_elements(By.CSS_SELECTOR, "a[href^='/']"): | |
| href = el.get_attribute("href") or "" | |
| if href.startswith(f"{BASE_URL}/") and not any(x in href for x in [ | |
| "login", "settings", "search", "blog", "reviews", "interviews", | |
| "available", "find-massage", "live-cams", "sitemap", "advertise", | |
| "mailbox", "static", "_next", "images", | |
| ]): | |
| parts = href.replace(f"{BASE_URL}/", "").split("/") | |
| if parts and parts[0] and len(parts) == 1: | |
| avail_links.append(parts[0].lower()) | |
| avail_position = None | |
| for i, uname in enumerate(avail_links): | |
| if uname == username_lower: | |
| avail_position = i + 1 | |
| break | |
| total = len(profile_links) | |
| total_avail = len(avail_links) | |
| result = { | |
| "city": city, | |
| "position": position, | |
| "total": total, | |
| "available_position": avail_position, | |
| "available_total": total_avail, | |
| } | |
| log.info("Search position: #%d/%d (available: #%s/%d) in %s", | |
| position or -1, total, | |
| avail_position or "N/A", total_avail, city) | |
| log_search_position(city, position or 0, total, total_avail, USERNAME) | |
| if position and position > 20: | |
| log_alert("ranking_drop", "warning", | |
| f"Position dropped to #{position} in {city}") | |
| return result | |
| # --------------------------------------------------------------------------- | |
| # Daemon | |
| # --------------------------------------------------------------------------- | |
| class TrafficEngine: | |
| """Main daemon loop.""" | |
| def __init__(self, watch_city=None): | |
| self.browser = Browser() | |
| self.watch_city = watch_city | |
| self.running = True | |
| self.last_refresh = 0 | |
| self.last_stats = 0 | |
| self.last_search = 0 | |
| self.prev_stats = None | |
| def cycle(self): | |
| """One daemon cycle — check what needs doing.""" | |
| now = time.time() | |
| # Availability refresh | |
| if now - self.last_refresh >= REFRESH_INTERVAL: | |
| try: | |
| self.browser.set_available(duration=1) | |
| self.last_refresh = now | |
| except Exception as e: | |
| log.error("Availability refresh failed: %s", e) | |
| log_availability("set", success=False, detail=str(e)) | |
| # Reset browser on error | |
| self.browser.stop() | |
| time.sleep(5) | |
| # Stats collection | |
| if now - self.last_stats >= STAT_INTERVAL: | |
| try: | |
| stats = self.browser.get_stats() | |
| if stats: | |
| log_stats( | |
| stats["profile_views"], | |
| stats["contact_clicks"], | |
| stats["online_bookmarks"], | |
| stats["who_saw_me"], | |
| stats["membership_status"], | |
| stats["visibility"], | |
| stats["availability_status"], | |
| stats["availability_remaining"], | |
| ) | |
| log.info("Stats: views=%s contacts=%s bookmarks=%s who_saw=%s | %s | vis=%s | avail=%s %s", | |
| stats["profile_views"], | |
| stats["contact_clicks"], | |
| stats["online_bookmarks"], | |
| stats["who_saw_me"], | |
| stats["membership_status"], | |
| stats["visibility"], | |
| stats["availability_status"], | |
| stats["availability_remaining"]) | |
| # Alert on changes | |
| if self.prev_stats: | |
| if stats["visibility"] == "hidden" and self.prev_stats.get("visibility") != "hidden": | |
| log_alert("visibility_hidden", "critical", | |
| "Profile is HIDDEN — traffic will drop to zero") | |
| if stats["availability_status"] != "Available": | |
| log_alert("not_available", "warning", | |
| f"Availability is {stats['availability_status']}") | |
| self.prev_stats = stats | |
| self.last_stats = now | |
| except Exception as e: | |
| log.error("Stats collection failed: %s", e) | |
| # Search position check | |
| if self.watch_city and (now - self.last_search >= SEARCH_CHECK_INTERVAL): | |
| try: | |
| self.browser.check_search_position(self.watch_city) | |
| self.last_search = now | |
| except Exception as e: | |
| log.error("Search position check failed: %s", e) | |
| def run_once(self): | |
| """Single cycle — for --once mode.""" | |
| log.info("=== Single cycle ===") | |
| if not self.browser.login(): | |
| log.error("Login failed, aborting") | |
| return | |
| self.last_refresh = 0 | |
| self.last_stats = 0 | |
| self.last_search = 0 | |
| self.cycle() | |
| self.browser.stop() | |
| def run_daemon(self): | |
| """Main daemon loop.""" | |
| log.info("=== Traffic Engine starting (daemon mode) ===") | |
| log.info("Refresh interval: %d min | Stats: %d min | Search: %d min", | |
| REFRESH_INTERVAL // 60, STAT_INTERVAL // 60, SEARCH_CHECK_INTERVAL // 60) | |
| if self.watch_city: | |
| log.info("Watching search position in: %s", self.watch_city) | |
| if not self.browser.login(): | |
| log.error("Initial login failed, retrying in 60s...") | |
| time.sleep(60) | |
| if not self.browser.login(): | |
| log.error("Login failed twice, exiting") | |
| return | |
| self.last_refresh = time.time() - REFRESH_INTERVAL + 10 # refresh soon | |
| self.last_stats = time.time() - STAT_INTERVAL + 10 | |
| if self.watch_city: | |
| self.last_search = time.time() - SEARCH_CHECK_INTERVAL + 10 | |
| while self.running: | |
| try: | |
| self.cycle() | |
| except KeyboardInterrupt: | |
| log.info("Shutdown requested") | |
| self.running = False | |
| break | |
| except Exception as e: | |
| log.error("Cycle error: %s", e) | |
| self.browser.stop() | |
| time.sleep(30) | |
| self.browser.login() | |
| time.sleep(30) # check every 30s | |
| self.browser.stop() | |
| log.info("Engine stopped") | |
| # --------------------------------------------------------------------------- | |
| # Stats Report | |
| # --------------------------------------------------------------------------- | |
| def print_stats(): | |
| conn = db() | |
| print("\n=== TRAFFIC STATS SUMMARY ===\n") | |
| # Availability history | |
| rows = conn.execute( | |
| "SELECT * FROM availability_log ORDER BY ts DESC LIMIT 20" | |
| ).fetchall() | |
| print(f"--- Availability (last {len(rows)} events) ---") | |
| for r in rows: | |
| print(f" {r['ts'][:19]} | {r['action']:6s} | status={r['status'] or '-':12s} " | |
| f"remaining={r['hours'] or 0}h{r['minutes'] or 0}m | success={bool(r['success'])}") | |
| # Stats history | |
| rows = conn.execute( | |
| "SELECT * FROM stats_log ORDER BY ts DESC LIMIT 20" | |
| ).fetchall() | |
| print(f"\n--- Profile Stats (last {len(rows)} samples) ---") | |
| for r in rows: | |
| print(f" {r['ts'][:19]} | views={r['profile_views'] or '-':6} " | |
| f"contacts={r['contact_clicks'] or '-':4} " | |
| f"bookmarks={r['online_bookmarks'] or '-':3} " | |
| f"who_saw={r['who_saw_me'] or '-':3} " | |
| f"| {r['availability_status'] or '-':12s} {r['availability_remaining'] or ''} " | |
| f"| vis={r['visibility'] or '-'}") | |
| # Search positions | |
| rows = conn.execute( | |
| "SELECT * FROM search_position ORDER BY ts DESC LIMIT 20" | |
| ).fetchall() | |
| if rows: | |
| print(f"\n--- Search Positions (last {len(rows)} checks) ---") | |
| for r in rows: | |
| print(f" {r['ts'][:19]} | {r['city']:20s} | " | |
| f"position=#{r['position']}/{r['total_results']} " | |
| f"| available=#{r['available_now'] or 'N/A'}") | |
| # Alerts | |
| rows = conn.execute( | |
| "SELECT * FROM alerts WHERE acknowledged=0 ORDER BY ts DESC LIMIT 10" | |
| ).fetchall() | |
| if rows: | |
| print(f"\n--- Active Alerts ({len(rows)}) ---") | |
| for r in rows: | |
| print(f" {r['ts'][:19]} | [{r['severity']:8s}] {r['type']:20s} | {r['message']}") | |
| # Trend: views over time | |
| rows = conn.execute( | |
| "SELECT ts, profile_views FROM stats_log " | |
| "WHERE profile_views IS NOT NULL ORDER BY ts ASC LIMIT 50" | |
| ).fetchall() | |
| if len(rows) >= 2: | |
| first = rows[0] | |
| last = rows[-1] | |
| delta = (last["profile_views"] or 0) - (first["profile_views"] or 0) | |
| print(f"\n--- View Trend ---") | |
| print(f" {first['ts'][:19]}: {first['profile_views']} views") | |
| print(f" {last['ts'][:19]}: {last['profile_views']} views") | |
| print(f" Delta: +{delta} views over {len(rows)} samples") | |
| conn.close() | |
| # --------------------------------------------------------------------------- | |
| # CLI | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| parser = argparse.ArgumentParser(description="RentMasseur Traffic Engine") | |
| parser.add_argument("--once", action="store_true", help="Run single cycle and exit") | |
| parser.add_argument("--stats", action="store_true", help="Print stats summary and exit") | |
| parser.add_argument("--watch", metavar="CITY", help="Track search position in city (e.g. 'Manhattan, NY')") | |
| parser.add_argument("--duration", type=int, default=1, help="Availability duration in hours (default: 1)") | |
| args = parser.parse_args() | |
| init_db() | |
| if args.stats: | |
| print_stats() | |
| return | |
| engine = TrafficEngine(watch_city=args.watch) | |
| if args.once: | |
| engine.run_once() | |
| else: | |
| engine.run_daemon() | |
| if __name__ == "__main__": | |
| main() | |