""" BTGO Scraper - Automatizare citire solduri conturi bancare Folosește Playwright pentru automatizare browser """ from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout import logging import csv import json import time import os from datetime import datetime from pathlib import Path from config import Config class BTGoScraper: """Scraper pentru extragerea soldurilor de pe btgo.ro""" def __init__(self): """Initializare scraper cu logging si configurare""" self.config = Config() Config.validate() self._setup_logging() self._ensure_directories() self.page = None self.login_page = None # Pagina de login (popup) # Setup pentru progress updates prin Telegram (optional) self.telegram_chat_id = os.getenv('TELEGRAM_CHAT_ID') self.telegram_message_id = os.getenv('TELEGRAM_MESSAGE_ID') self.progress_notifier = None logging.info(f"Environment: TELEGRAM_CHAT_ID={self.telegram_chat_id}, TELEGRAM_MESSAGE_ID={self.telegram_message_id}") # Inițializează notifier pentru progress updates dacă avem chat_id if self.telegram_chat_id and self.telegram_message_id: try: from notifications import TelegramNotifier self.progress_notifier = TelegramNotifier(self.config) logging.info(f"Progress updates activate pentru chat_id={self.telegram_chat_id}, message_id={self.telegram_message_id}") except Exception as e: logging.warning(f"Nu am putut inițializa progress notifier: {e}") else: logging.warning("Progress updates dezactivate - lipsesc TELEGRAM_CHAT_ID sau TELEGRAM_MESSAGE_ID") def _setup_logging(self): """Configurare logging in consola si fisier zilnic""" # Creaza director logs daca nu exista Path(self.config.LOG_DIR).mkdir(exist_ok=True) # Nume fisier cu data curenta log_file = Path(self.config.LOG_DIR) / f"scraper_{datetime.now().strftime('%Y-%m-%d')}.log" # Format log log_format = '[%(asctime)s] [%(levelname)s] %(message)s' date_format = '%Y-%m-%d %H:%M:%S' # Configurare logging logging.basicConfig( level=getattr(logging, self.config.LOG_LEVEL), format=log_format, datefmt=date_format, handlers=[ logging.StreamHandler(), # Console logging.FileHandler(log_file, encoding='utf-8') # Fisier ] ) logging.info(f"Logging initializat: {log_file}") def _ensure_directories(self): """Creaza directoarele data/ si logs/ daca nu exista""" Path(self.config.OUTPUT_DIR).mkdir(exist_ok=True) Path(self.config.LOG_DIR).mkdir(exist_ok=True) logging.info(f"Directoare verificate: {self.config.OUTPUT_DIR}, {self.config.LOG_DIR}") def _update_progress(self, message: str): """ Trimite update de progres către Telegram (editează mesajul inițial) Args: message: Mesajul de progres """ if self.progress_notifier and self.telegram_chat_id and self.telegram_message_id: full_message = f"*{message}*" self.progress_notifier._edit_message( self.telegram_chat_id, self.telegram_message_id, full_message ) logging.info(f"Progress update: {message}") def run(self): """Entry point principal - orchestreaza tot flow-ul""" try: # Check dacă rulăm în mod balances_only balances_only = os.getenv('BALANCES_ONLY', 'false').lower() == 'true' logging.info("=" * 60) if balances_only: logging.info("Start BTGO Scraper (DOAR SOLDURI)") else: logging.info("Start BTGO Scraper") logging.info("=" * 60) with sync_playwright() as p: # Lansare browser browser = p.chromium.launch( headless=self.config.HEADLESS, slow_mo=100 if not self.config.HEADLESS else 0 # Slow motion pentru debugging ) # Creaza pagina cu viewport standard self.page = browser.new_page(viewport={'width': 1920, 'height': 1080}) logging.info(f"Browser lansat (headless={self.config.HEADLESS})") # Flow complet self.login() self.handle_2fa_wait() self._update_progress("2FA aprobat! Incarc conturi...") accounts = self.read_accounts() csv_path, json_path = self.save_results(accounts) # Descarcă tranzacții pentru toate conturile (doar dacă nu e balances_only) downloaded_files = [] if balances_only: logging.info("Mod DOAR SOLDURI - skip download tranzactii") elif self.config.DOWNLOAD_TRANSACTIONS: downloaded_files = self.download_transactions(accounts) else: logging.info("Download tranzacții dezactivat (DOWNLOAD_TRANSACTIONS=false)") # Screenshot final de confirmare screenshot_path = Path(self.config.OUTPUT_DIR) / f"dashboard_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.png" self.page.screenshot(path=str(screenshot_path)) logging.info(f"Screenshot final salvat: {screenshot_path}") # Trimite notificări (email, Discord) if self.config.ENABLE_NOTIFICATIONS: self.send_notifications(csv_path, downloaded_files, accounts) # Afișează rezumat final logging.info("=" * 60) logging.info("REZUMAT FINAL") logging.info("=" * 60) logging.info(f"Conturi citite: {len(accounts)}") if self.config.DOWNLOAD_TRANSACTIONS: logging.info(f"Tranzacții descărcate: {len(downloaded_files)}") for df in downloaded_files: logging.info(f" - {df['cont']}: {df['fisier']}") logging.info("=" * 60) browser.close() # Nu mai trimitem update progress aici - mesajul final cu soldurile a fost deja editat de notifications.py logging.info("=" * 60) logging.info("✓ Scraper finalizat cu succes!") logging.info("=" * 60) return True except Exception as e: self._handle_error(e) return False def login(self): """Autentificare cu username si password""" self._update_progress("Deschid pagina de login...") logging.info("Navigare catre https://go.bancatransilvania.ro/") self.page.goto('https://go.bancatransilvania.ro/', wait_until='networkidle') logging.info("Pagina incarcata") try: # Cookie consent - asteapta si accepta logging.info("Acceptare cookies...") try: cookie_button = self.page.get_by_role("button", name="Sunt de acord", exact=True) cookie_button.click(timeout=5000) logging.info("✓ Cookies acceptate") except: logging.info("Nu a fost necesar acceptul cookies (posibil deja acceptat)") # Click pe butonul LOGIN - deschide popup logging.info("Click pe butonul LOGIN...") with self.page.expect_popup() as popup_info: login_link = self.page.get_by_role("link", name="LOGIN") login_link.click() # Preia referinta la popup-ul de login self.login_page = popup_info.value logging.info("✓ Popup login deschis") # Completare username logging.info("Completare username...") username_field = self.login_page.get_by_placeholder("ID de logare") username_field.fill(self.config.BTGO_USERNAME) logging.info("✓ Username completat") # Completare password logging.info("Completare password...") password_field = self.login_page.get_by_placeholder("Parola") password_field.fill(self.config.BTGO_PASSWORD) logging.info("✓ Password completat") # Click pe butonul de submit logging.info("Click pe 'Mergi mai departe'...") submit_button = self.login_page.get_by_role("button", name="Mergi mai departe") submit_button.click() logging.info("✓ Credentials trimise, astept 2FA...") self._update_progress("Astept aprobare 2FA pe telefon...") except PlaywrightTimeout as e: logging.error(f"Timeout la login: {e}") raise Exception("Nu am gasit elementele de login. Verifica selectors!") except Exception as e: logging.error(f"Eroare la login: {e}") raise def handle_2fa_wait(self): """Asteapta aprobare 2FA cu auto-detect""" logging.info("=" * 60) logging.info("🔐 APROBARE 2FA NECESARA") logging.info("=" * 60) logging.info("Verifica aplicatia George pe telefon si aproba autentificarea.") logging.info(f"Timeout: {self.config.TIMEOUT_2FA_SECONDS} secunde") logging.info("=" * 60) # Detectam login reusit prin schimbarea URL-ului la goapp.bancatransilvania.ro # sau prin aparitia butonului de conturi (#accountsBtn) post_login_selectors = [ "#accountsBtn", # Butonul pentru conturi (apare post-login) ] start_time = datetime.now() timeout = self.config.TIMEOUT_2FA_SECONDS while (datetime.now() - start_time).seconds < timeout: # Verifică dacă butonul de conturi este CLICKABLE (nu doar vizibil) # Aceasta este singura verificare sigură - butonul apare doar după 2FA reușit try: # Așteaptă ca #accountsBtn să fie clickable (nu doar vizibil în DOM) accounts_btn = self.login_page.locator("#accountsBtn") if accounts_btn.is_visible(timeout=1000): # Verifică că este și clickable (enabled) if accounts_btn.is_enabled(): logging.info("✓ Autentificare 2FA reusita! (Buton conturi activ)") time.sleep(2) # Asteapta ca pagina sa se stabilizeze complet # Update page reference la login_page pentru restul operatiilor self.page = self.login_page return True except: pass # Afiseaza countdown elapsed = (datetime.now() - start_time).seconds remaining = timeout - elapsed print(f"\rAsteptam aprobare 2FA... {remaining}s ramas", end='', flush=True) time.sleep(2) print() # New line dupa countdown raise TimeoutError(f"Timeout 2FA dupa {timeout} secunde. Verifica ca ai aprobat pe telefon!") def download_transactions(self, accounts): """Descarca CSV-uri cu tranzactiile pentru fiecare cont""" logging.info("=" * 60) logging.info("Descarcare tranzactii pentru toate conturile...") logging.info("=" * 60) downloaded_files = [] # IMPORTANT: Collapse toate conturile mai intai logging.info("Collapse toate conturile...") all_expanded = self.page.locator(".mat-icon.rotate-90").all() for expanded_icon in all_expanded: try: expanded_icon.click() time.sleep(0.3) except: pass time.sleep(1) logging.info("✓ Toate conturile sunt collapse") # Re-gaseste toate cardurile de conturi all_cards = self.page.locator("fba-account-details-card").all() logging.info(f"Gasit {len(all_cards)} carduri de conturi") for idx, account in enumerate(accounts, 1): try: nume_cont = account['nume_cont'] iban = account['iban'] self._update_progress(f"Descarc tranzactii ({idx}/{len(accounts)})...") logging.info(f"[{idx}/{len(accounts)}] Descarcare tranzactii pentru: {nume_cont}") # Doar pentru PRIMUL cont trebuie expand + click Tranzacții # Pentru restul, suntem deja pe pagina de tranzacții (din selectarea din modal) if idx == 1: # Primul cont - expand și click Tranzacții if idx - 1 >= len(all_cards): logging.error(f" ✗ Nu exista card la pozitia {idx-1}") continue card = all_cards[idx - 1] # Expand contul (click pe săgeată) expand_button = card.locator(".collapse-account-btn").first expand_button.click() time.sleep(2) # Așteaptă expandare logging.info(f" Contul expandat") # Click pe butonul Tranzacții try: transactions_button = card.locator(".account-transactions-btn").first transactions_button.click() time.sleep(3) # Așteaptă încărcarea paginii cu tranzacții logging.info(f" Click pe buton Tranzactii - pagina se incarca...") except Exception as e: logging.error(f" ✗ Nu am gasit butonul Tranzactii: {e}") try: expand_button.click() time.sleep(0.5) except: pass continue else: # Conturile 2-5: suntem deja pe pagina de tranzacții (din modal) logging.info(f" Deja pe pagina tranzactii (selectat din modal)") time.sleep(2) # Așteaptă stabilizare pagină # Așteaptă să apară butonul CSV (indica că pagina s-a încărcat) try: self.page.wait_for_selector('button:has-text("CSV")', timeout=5000) logging.info(f" Buton CSV detectat") except: logging.warning(f" Timeout asteptand butonul CSV") # Click pe butonul CSV și așteaptă download try: with self.page.expect_download(timeout=15000) as download_info: csv_button = self.page.get_by_role("button", name="CSV") csv_button.click() logging.info(f" Click pe butonul CSV - astept download...") download = download_info.value # Salvează fișierul cu un nume descriptiv timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') nume_safe = nume_cont.replace(' ', '_').replace('/', '_') filename = f"tranzactii_{nume_safe}_{timestamp}.csv" save_path = Path(self.config.OUTPUT_DIR) / filename download.save_as(save_path) logging.info(f" ✓ Salvat: {save_path}") downloaded_files.append({ 'cont': nume_cont, 'iban': iban, 'fisier': str(save_path) }) except Exception as e: logging.error(f" ✗ Eroare la descarcarea CSV: {e}") # Navighează înapoi la lista de conturi try: # Click pe butonul back/close (săgeată stânga sau X) back_button = self.page.locator('button[aria-label="Back"], .back-button, #selectAccountBtn').first back_button.click() time.sleep(1.5) logging.info(f" Navigat inapoi - verific modal...") except Exception as e: logging.warning(f" Nu am putut naviga inapoi: {e}") time.sleep(1) # Verifică dacă a apărut modal de selectare cont try: modal_visible = self.page.locator('.modal-content').is_visible(timeout=2000) if modal_visible and idx < len(accounts): logging.info(f" Modal detectat - selectez contul urmator...") # Calculează ID-ul contului următor next_account = accounts[idx] # idx este 0-indexed pentru next next_iban = next_account['iban'] next_iban_digits = ''.join(filter(str.isdigit, next_iban))[-10:] next_account_id = f"accountC14RONCRT{next_iban_digits}" # Click pe contul următor din modal modal_account = self.page.locator(f'#{next_account_id}').first modal_account.click() time.sleep(2) logging.info(f" ✓ Selectat cont din modal: {next_account['nume_cont']}") else: # Nu e modal - e ultima iteratie sau nu a aparut modal logging.info(f" Nu e modal - continuam normal") except Exception as e: logging.warning(f" Eroare verificare modal: {e}") # Re-găsește cardurile (pentru flow normal fără modal) try: all_cards = self.page.locator("fba-account-details-card").all() except: pass except Exception as e: logging.error(f" ✗ Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}") # Încearcă să navighezi înapoi try: self.page.keyboard.press("Escape") time.sleep(1) except: pass continue logging.info("=" * 60) logging.info(f"✓ Descarcate {len(downloaded_files)}/{len(accounts)} fisiere CSV cu tranzactii") logging.info("=" * 60) return downloaded_files def read_accounts(self): """Extrage soldurile tuturor conturilor""" logging.info("Citire conturi si solduri...") try: # Click pe butonul de conturi pentru a afisa lista logging.info("Click pe butonul Conturi...") # Simplu selector - doar butonul, nu div-ul interior accounts_button = self.page.locator("#accountsBtn") accounts_button.click() time.sleep(3) # Asteapta ca lista sa se incarce logging.info("✓ Sectiunea conturi deschisa") # Update progres DUPĂ ce lista de conturi s-a încărcat self._update_progress("Citesc solduri conturi...") # Gaseste toate cardurile de conturi account_cards = self.page.locator("fba-account-details-card").all() logging.info(f"Gasit {len(account_cards)} conturi") accounts = [] for idx, card in enumerate(account_cards, 1): try: # Extrage nume cont (din tag