Created
November 25, 2025 09:28
-
-
Save me-suzy/e7db602c1d4eca4de9992f5037e12461 to your computer and use it in GitHub Desktop.
Claude-FINAL 13 - BUN Sterge pdf pe G BUN.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Automatizare descărcare PDF-uri din Arcanum (FIXED VERSION cu SORTARE CRONOLOGICĂ): | |
| - FIXED: Scanează corect toate fișierele existente de pe disk | |
| - FIXED: Păstrează progresul parțial între zile | |
| - FIXED: Procesează și combină corect TOATE PDF-urile pentru fiecare issue | |
| - FIXED: Resume logic corect pentru issue-urile parțiale | |
| - FIXED: Detectează corect prefix-urile pentru fișiere | |
| - FIXED: Verifică corect issue-urile complete pentru skip URLs | |
| - FIXED: Elimină dublurile automat | |
| - FIXED: Detectează mai bine numărul total de pagini | |
| - FIXED: Sortare cronologică corectă în downloaded_issues | |
| - FIXED: Detectează și oprește automat pentru CAPTCHA | |
| - FIXED: Așteaptă automat pentru mentenanță (403 Forbidden) | |
| - FIXED: Gestionează pop-up Windows de autentificare | |
| DEPENDENȚE OPȚIONALE: | |
| - pyautogui: Pentru gestionarea automată a pop-up-urilor Windows de autentificare | |
| Instalare: pip install pyautogui | |
| (Scriptul funcționează și fără, dar va aștepta intervenție manuală) | |
| """ | |
| import time | |
| import os | |
| import sys | |
| import re | |
| import json | |
| import shutil | |
| import subprocess | |
| from datetime import datetime | |
| from selenium import webdriver | |
| from selenium.webdriver.common.by import By | |
| from selenium.webdriver.common.keys import Keys | |
| from selenium.webdriver.support.ui import WebDriverWait | |
| from selenium.webdriver.support import expected_conditions as EC | |
| from selenium.webdriver.chrome.options import Options | |
| from selenium.common.exceptions import WebDriverException, ElementClickInterceptedException | |
| import logging | |
| import sys | |
| def setup_logging(): | |
| """Configurează logging în timp real""" | |
| log_dir = r"E:\Carte\BB\17 - Site Leadership\alte\Ionel Balauta\Aryeht\Task 1 - Traduce tot site-ul\Doar Google Web\Andreea\Meditatii\2023\++Arcanum Download + Chrome\Ruleaza cand sunt plecat 3\Logs" | |
| os.makedirs(log_dir, exist_ok=True) | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| log_file = os.path.join(log_dir, f"arcanum_download_{timestamp}.log") | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(message)s', | |
| datefmt='%Y-%m-%d %H:%M:%S', | |
| handlers=[ | |
| logging.FileHandler(log_file, mode='w', encoding='utf-8'), | |
| logging.StreamHandler(sys.stdout) | |
| ] | |
| ) | |
| for handler in logging.getLogger().handlers: | |
| if isinstance(handler, logging.FileHandler): | |
| handler.stream.reconfigure(line_buffering=True) | |
| print(f"📝 LOGGING ACTIVAT: {log_file}") | |
| return log_file | |
| # Colecțiile adiționale (procesate DUPĂ colecția principală din main()) | |
| ADDITIONAL_COLLECTIONS = [ | |
| # "https://adt.arcanum.com/ro/collection/StudiiSiCercetariMecanicaSiAplicata/" | |
| # "https://adt.arcanum.com/ro/collection/Farmacia/", | |
| # "https://adt.arcanum.com/ro/collection/RevistaConstructilorMaterialelorDeConstructii/", | |
| # "https://adt.arcanum.com/de/collection/Mathematische/", | |
| # "https://adt.arcanum.com/ro/collection/GInfo/", | |
| # "https://adt.arcanum.com/ro/collection/Electrotehnica/", | |
| # "https://adt.arcanum.com/ro/collection/Hidrotechnica/", | |
| # "https://adt.arcanum.com/ro/collection/ZiarulStiintelorSiCalatoriilor/", | |
| # "https://adt.arcanum.com/ro/collection/Energetica/", | |
| # "https://adt.arcanum.com/ro/collection/AgriculturaSalajului/", | |
| # "https://adt.arcanum.com/ro/collection/FoiaDeAgriculturaPractica/", | |
| # "https://adt.arcanum.com/ro/collection/RevistaConstructilorMaterialelorDeConstructii/", | |
| # "https://adt.arcanum.com/ro/collection/ConstructiaDeMasini/", | |
| # "https://adt.arcanum.com/en/collection/ByteRomania/", | |
| # "https://adt.arcanum.com/ro/collection/RevueRoumaineDeChimie/", | |
| # "https://adt.arcanum.com/ro/collection/Almanahul_StiintaSiTehnica/", | |
| # "https://adt.arcanum.com/ro/collection/StudiiSiCercetariDeEnergetica/", | |
| # "https://adt.arcanum.com/ro/collection/Telecomunicatii/", | |
| # "https://adt.arcanum.com/ro/collection/PcReport/", | |
| # "https://adt.arcanum.com/ro/collection/StiintaSiTehnica/", | |
| # "https://adt.arcanum.com/ro/collection/CulturaFizicaSiSport/", | |
| # "https://adt.arcanum.com/ro/collection/UniversulSport/", | |
| # "https://adt.arcanum.com/ro/collection/UniversulSport/?decade=1940#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/ProSport/", | |
| "https://adt.arcanum.com/ro/collection/ProSport/?decade=2000#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/ProSport/?decade=2010#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/SportVest/", | |
| "https://adt.arcanum.com/ro/collection/Almanahul_Satelor/", | |
| "https://adt.arcanum.com/ro/collection/Minimum/", | |
| "https://adt.arcanum.com/ro/collection/TelegrafulRoman/", | |
| "https://adt.arcanum.com/ro/collection/SteauaRomaniei/", | |
| "https://adt.arcanum.com/ro/collection/MarosvasarhelyProEuropa/", | |
| "https://adt.arcanum.com/ro/collection/RevistaCailorFerate/", | |
| "https://adt.arcanum.com/ro/collection/Arhitectura/", | |
| "https://adt.arcanum.com/ro/collection/RevistaDeFolclor/", | |
| "https://adt.arcanum.com/ro/collection/AlmanahBTT/", | |
| "https://adt.arcanum.com/ro/collection/MinePetrolGaze/", | |
| "https://adt.arcanum.com/ro/collection/PostaTelekomunicatiii/", | |
| "https://adt.arcanum.com/ro/collection/LimbaRomana/", | |
| "https://adt.arcanum.com/ro/collection/BuletinulInstitutuluiPolitehnicBucuresti_Mecanica/", | |
| "https://adt.arcanum.com/ro/collection/RevistaMinelor/", | |
| "https://adt.arcanum.com/ro/collection/StudiiSiCercetariDeGeologie/", | |
| "https://adt.arcanum.com/ro/collection/RevistaIndustriaAlimentare/", | |
| "https://adt.arcanum.com/ro/collection/IndustriaLemnului/", | |
| "https://adt.arcanum.com/ro/collection/Electricitatea/", | |
| "https://adt.arcanum.com/ro/collection/SzatmariMuzeumKiadvanyai_Evkonyv_ADT/", | |
| "https://adt.arcanum.com/ro/collection/ConstructiaDeMasini/", | |
| "https://adt.arcanum.com/ro/collection/SufletNou/", | |
| "https://adt.arcanum.com/ro/collection/Rondul/", | |
| "https://adt.arcanum.com/en/collection/MTA_ActaMicrobiologica/", | |
| "https://adt.arcanum.com/ro/collection/RomaniaMilitara/", | |
| "https://adt.arcanum.com/ro/collection/Magazin/", | |
| "https://adt.arcanum.com/ro/collection/TribunaSibiului/", | |
| "https://adt.arcanum.com/ro/collection/Metalurgia/", | |
| "https://adt.arcanum.com/ro/collection/RevistaEconomica1974/", | |
| "https://adt.arcanum.com/ro/collection/StudiiSiCercetariDeMetalurgie/", | |
| "https://adt.arcanum.com/ro/collection/BuletinulInstitutuluiPolitehnicBucuresti_ChimieMetalurgie/", | |
| "https://adt.arcanum.com/ro/collection/RomaniaMuncitoare/", | |
| "https://adt.arcanum.com/ro/collection/Cronica/", | |
| "https://adt.arcanum.com/ro/collection/ProblemeDeInformareSiDocumentare/", | |
| "https://adt.arcanum.com/ro/collection/GazetaInvantamantului/", | |
| "https://adt.arcanum.com/ro/collection/Marisia_ADT/", | |
| "https://adt.arcanum.com/ro/collection/ZiarulDeMures/", | |
| "https://adt.arcanum.com/ro/collection/RevistaDeEtnografieSiFolclor/", | |
| "https://adt.arcanum.com/ro/collection/RevistaMuzeelor/", | |
| "https://adt.arcanum.com/ro/collection/Constructii/", | |
| "https://adt.arcanum.com/ro/collection/GazetaDeDuminica/", | |
| "https://adt.arcanum.com/ro/collection/BuletInstPolitehIasi_0/", | |
| "https://adt.arcanum.com/ro/collection/Fotbal/", | |
| "https://adt.arcanum.com/ro/collection/JurnalulNational/", | |
| "https://adt.arcanum.com/ro/collection/UnireaBlajPoporului/", | |
| "https://adt.arcanum.com/ro/collection/TranssylvaniaNostra/", | |
| "https://adt.arcanum.com/ro/collection/CuvantulPoporului/", | |
| "https://adt.arcanum.com/ro/collection/Agrarul/", | |
| "https://adt.arcanum.com/ro/collection/RevistaPadurilor/", | |
| "https://adt.arcanum.com/ro/collection/RomaniaEroica/", | |
| "https://adt.arcanum.com/ro/collection/Radiofonia/", | |
| "https://adt.arcanum.com/ro/collection/TransilvaniaBusiness/", | |
| "https://adt.arcanum.com/ro/collection/CurierulDeIasi/", | |
| "https://adt.arcanum.com/ro/collection/BuletinulUniversitatiiDinBrasov/", | |
| "https://adt.arcanum.com/ro/collection/CurierulFoaiaIntereselorGenerale/", | |
| "https://adt.arcanum.com/ro/collection/EconomiaNationala/", | |
| "https://adt.arcanum.com/ro/collection/Constitutionalul/", | |
| "https://adt.arcanum.com/ro/collection/Semnalul/", | |
| "https://adt.arcanum.com/ro/collection/Rampa/", | |
| "https://adt.arcanum.com/ro/collection/ViataRomaneasca/", | |
| "https://adt.arcanum.com/ro/collection/SteauaRosie/", | |
| "https://adt.arcanum.com/ro/collection/Indreptarea/", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1830#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1840#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1850#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1860#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1870#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1880#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1890#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1900#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1910#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1920#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1930#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/GazetaTransilvaniei/?decade=1940#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/Albina/", | |
| "https://adt.arcanum.com/ro/collection/Ateneu/", | |
| "https://adt.arcanum.com/ro/collection/Universul/", | |
| "https://adt.arcanum.com/ro/collection/BuletInstPolitehIasi_5/", | |
| "https://adt.arcanum.com/ro/collection/BuletInstPolitehIasi_4/", | |
| "https://adt.arcanum.com/ro/collection/BuletInstPolitehIasi_3/", | |
| "https://adt.arcanum.com/ro/collection/BuletInstPolitehIasi_0/", | |
| "https://adt.arcanum.com/ro/collection/BuletInstPolitehIasi_6/", | |
| "https://adt.arcanum.com/ro/collection/BuletInstPolitehIasi_7/", | |
| "https://adt.arcanum.com/ro/collection/BuletInstPolitehIasi_8/", | |
| "https://adt.arcanum.com/ro/collection/EraSocialista/", | |
| "https://adt.arcanum.com/ro/collection/ViataStudenteasca/", | |
| "https://adt.arcanum.com/ro/collection/ViataStudenteasca/?decade=1980#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/Almanah_ScinteiaTineretului/", | |
| "https://adt.arcanum.com/ro/collection/MuzeulDigitalAlRomanuluiRomanesc/", | |
| "https://adt.arcanum.com/ro/collection/EvenimentulZilei/", | |
| "https://adt.arcanum.com/ro/collection/Argus/", | |
| "https://adt.arcanum.com/ro/collection/Convietuirea/", | |
| "https://adt.arcanum.com/ro/collection/AlfaMagazin/", | |
| "https://adt.arcanum.com/ro/collection/CurierulFoaiaIntereselorGenerale/", | |
| "https://adt.arcanum.com/hu/collection/IdegenNyelvekTanitasa/", | |
| "https://adt.arcanum.com/hu/collection/AzEnekZeneTanitasa/", | |
| "https://adt.arcanum.com/hu/collection/ABiologiaTanitasa/", | |
| "https://adt.arcanum.com/hu/collection/ErtekezesekEmlekezesek/", | |
| "https://adt.arcanum.com/ro/collection/Transilvania/", | |
| "https://adt.arcanum.com/ro/collection/Buciumulu/", | |
| "https://adt.arcanum.com/ro/collection/Progresul/", | |
| "https://adt.arcanum.com/ro/collection/Apostrof/", | |
| "https://adt.arcanum.com/ro/collection/Flacara/", | |
| "https://adt.arcanum.com/ro/collection/Flacara/?decade=1960#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/Flacara/?decade=1970#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/Flacara/?decade=1980#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/Flacara/?decade=1990#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/Kamikaze/", | |
| "https://adt.arcanum.com/hu/collection/Books_SorozatonKivul/", | |
| "https://adt.arcanum.com/hu/collection/Books_22_OrvoslasTermeszetrajz/", | |
| "https://adt.arcanum.com/hu/collection/DomolkiKonyvek/", | |
| "https://adt.arcanum.com/de/collection/LiterarischeBerichteUngarn/", | |
| "https://adt.arcanum.com/ro/collection/BuletinulFacultatiiDeStiinteDinCernauti/", | |
| "https://adt.arcanum.com/sk/collection/SlovenskyZakonnik/", | |
| "https://adt.arcanum.com/sk/collection/Theologos/", | |
| "https://adt.arcanum.com/ro/collection/Cutezatorii/", | |
| "https://adt.arcanum.com/ro/collection/AparareaPatriei/", | |
| "https://adt.arcanum.com/ro/collection/AparareaPatriei/?decade=1960#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/AparareaPatriei/?decade=1970#collection-contents", | |
| "https://adt.arcanum.com/ro/collection/ZiDeZi/" | |
| ] | |
| # Skip URLs statice (hardcoded) | |
| STATIC_SKIP_URLS = { | |
| "https://adt.arcanum.com/ro/view/Convietuirea_1997-1998" | |
| } | |
| DAILY_LIMIT = 1050 | |
| STATE_FILENAME = "state.json" | |
| SKIP_URLS_FILENAME = "skip_urls.json" | |
| class ChromePDFDownloader: | |
| def __init__(self, main_collection_url, download_dir=None, batch_size=50, timeout=15): | |
| self.main_collection_url = main_collection_url | |
| self.batch_size = batch_size | |
| self.timeout = timeout | |
| self.download_dir = download_dir or os.getcwd() | |
| self.driver = None | |
| self.wait = None | |
| self.attached_existing = False | |
| self.state_path = os.path.join(self.download_dir, STATE_FILENAME) | |
| self.skip_urls_path = os.path.join(self.download_dir, SKIP_URLS_FILENAME) | |
| self.current_issue_url = None | |
| self.dynamic_skip_urls = set() | |
| # Tracking pentru retry-uri după CAPTCHA | |
| self.captcha_retry_count = {} # {segment_key: retry_count} | |
| self.captcha_wait_minutes = 7 # Timpul de așteptare după CAPTCHA (crescut la 7 minute) | |
| self.captcha_max_retries = 2 # Număr maxim de retry-uri înainte de oprire (0, 1, 2 = 3 încercări total) | |
| self.captcha_retry_needed = False # Flag pentru retry după CAPTCHA | |
| # Calea pentru loguri zilnice | |
| self.daily_log_dir = os.path.join(self.download_dir, "daily_logs") | |
| os.makedirs(self.daily_log_dir, exist_ok=True) | |
| # Crează backup zilnic ÎNAINTE de a încărca state-ul | |
| self._create_daily_backup() | |
| self._load_skip_urls() | |
| self._load_state() | |
| self.fix_existing_json() | |
| def _load_skip_urls(self): | |
| """Încarcă skip URLs din fișierul separat""" | |
| self.dynamic_skip_urls = set(STATIC_SKIP_URLS) # Începe cu cele statice | |
| if os.path.exists(self.skip_urls_path): | |
| try: | |
| # REPARĂ JSON-ul dacă are virgulă lipsă | |
| self._repair_json_missing_comma(self.skip_urls_path) | |
| with open(self.skip_urls_path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| completed_urls = data.get("completed_urls", []) | |
| completed_collections = data.get("completed_collections", []) | |
| self.dynamic_skip_urls.update(url.rstrip('/') for url in completed_urls) | |
| self.dynamic_skip_urls.update(url.rstrip('/') for url in completed_collections) | |
| print(f"📋 Încărcat {len(completed_urls)} URL-uri complet descărcate din {SKIP_URLS_FILENAME}") | |
| print(f"📋 Încărcat {len(completed_collections)} colecții complet procesate din {SKIP_URLS_FILENAME}") | |
| except Exception as e: | |
| print(f"⚠ Eroare la citirea {SKIP_URLS_FILENAME}: {e}") | |
| print(f"🔄 Recreez {SKIP_URLS_FILENAME} de la zero...") | |
| # Dacă JSON-ul e prea corupt, resetează-l | |
| self._save_skip_urls() | |
| print(f"🚫 Total URL-uri de skip: {len(self.dynamic_skip_urls)}") | |
| def _save_skip_urls(self): | |
| """FIXED: Verifică corect dacă un issue este complet - FOLOSEȘTE last_successful_segment_end!""" | |
| try: | |
| completed_urls = [] | |
| for item in self.state.get("downloaded_issues", []): | |
| # VERIFICARE CORECTĂ: folosește last_successful_segment_end, NU pages! | |
| completed_at = item.get("completed_at") | |
| total_pages = item.get("total_pages") | |
| last_segment = item.get("last_successful_segment_end", 0) | |
| pages = item.get("pages", 0) # Pentru debug | |
| # CONDIȚIE FIXATĂ: verifică progresul REAL (last_segment), nu pages! | |
| if (completed_at and # Marcat ca terminat | |
| total_pages and # Are total_pages setat | |
| total_pages > 0 and # Total valid | |
| last_segment >= total_pages): # Progresul REAL este complet | |
| completed_urls.append(item["url"]) | |
| print(f"✅ Issue complet pentru skip: {item['url']} ({last_segment}/{total_pages})") | |
| # DEBUG: Afișează discrepanțele | |
| if pages != last_segment: | |
| print(f" ⚠ DISCREPANȚĂ: pages={pages}, last_segment={last_segment}") | |
| else: | |
| # DEBUG: Afișează de ce nu e considerat complet | |
| if item.get("url"): # Doar dacă are URL valid | |
| print(f"🔄 Issue incomplet: {item.get('url', 'NO_URL')}") | |
| print(f" completed_at: {bool(completed_at)}") | |
| print(f" total_pages: {total_pages}") | |
| print(f" last_segment: {last_segment}") | |
| print(f" pages: {pages}") | |
| # Verifică fiecare condiție individual | |
| if not completed_at: | |
| print(f" → Lipsește completed_at") | |
| elif not total_pages or total_pages <= 0: | |
| print(f" → total_pages invalid") | |
| elif last_segment < total_pages: | |
| print(f" → Progres incomplet: {last_segment}/{total_pages}") | |
| # Adaugă și cele statice | |
| all_completed = list(STATIC_SKIP_URLS) + completed_urls | |
| # Păstrează și colecțiile complete dacă există | |
| existing_data = {} | |
| if os.path.exists(self.skip_urls_path): | |
| with open(self.skip_urls_path, "r", encoding="utf-8") as f: | |
| existing_data = json.load(f) | |
| data = { | |
| "last_updated": datetime.now().isoformat(), | |
| "completed_urls": sorted(list(set(all_completed))), | |
| "completed_collections": existing_data.get("completed_collections", []) | |
| } | |
| with open(self.skip_urls_path, "w", encoding="utf-8") as f: | |
| json.dump(data, f, indent=2, ensure_ascii=False) | |
| print(f"💾 Salvat {len(data['completed_urls'])} URL-uri CORECT VERIFICATE în {SKIP_URLS_FILENAME}") | |
| # RAPORT FINAL pentru debugging | |
| print(f"📋 ISSUES COMPLETE în skip_urls:") | |
| for url in sorted(completed_urls): | |
| year = url.split('_')[-1] if '_' in url else 'UNKNOWN' | |
| print(f" ✅ {year}") | |
| except Exception as e: | |
| print(f"⚠ Eroare la salvarea {SKIP_URLS_FILENAME}: {e}") | |
| def _safe_folder_name(self, name: str) -> str: | |
| return re.sub(r'[<>:"/\\|?*]', '_', name).strip() | |
| def _decode_unicode_escapes(self, obj): | |
| """Decodifică secvențele unicode din JSON""" | |
| if isinstance(obj, dict): | |
| return {key: self._decode_unicode_escapes(value) for key, value in obj.items()} | |
| elif isinstance(obj, list): | |
| return [self._decode_unicode_escapes(item) for item in obj] | |
| elif isinstance(obj, str): | |
| # Decodifică secvențele unicode ca \u0103, \u0219 | |
| try: | |
| return obj.encode('utf-8').decode('unicode_escape').encode('latin-1').decode('utf-8') if '\\u' in obj else obj | |
| except: | |
| return obj | |
| else: | |
| return obj | |
| def is_issue_complete_by_end_page(self, end_page): | |
| """FIXED: Determină dacă un issue e complet pe baza ultimei pagini""" | |
| # VERIFICARE CRITICĂ: Dacă e doar 1 pagină, probabil e o eroare de detectare | |
| if end_page <= 1: | |
| print(f"⚠ ALERTĂ: end_page={end_page} pare să fie o eroare de detectare!") | |
| return False # NU considera complet dacă e doar 1 pagină | |
| # Pentru issue-uri normale, verifică dacă ultima pagină nu e multiplu rotund | |
| return not ((end_page + 1) % 50 == 0 or (end_page + 1) % 100 == 0) | |
| def extract_issue_id_from_filename(self, filename): | |
| """FIXED: Extrage ID-ul issue-ului din numele fișierului (cu sau fără timestamp)""" | |
| # Încearcă primul pattern: cu timestamp (PrefixIssue-TIMESTAMP__pages) | |
| match = re.search(r'([^-]+(?:-[^-]+)*)-\d+__pages', filename) | |
| if match: | |
| return match.group(1) | |
| # Încearcă al doilea pattern: fără timestamp (PrefixIssue__pages) | |
| match = re.search(r'(.+?)__pages\d+-\d+\.pdf', filename) | |
| if match: | |
| return match.group(1) | |
| return None | |
| def extract_issue_url_from_filename(self, filename): | |
| """FIXED: Extrage URL-ul issue-ului din numele fișierului""" | |
| issue_id = self.extract_issue_id_from_filename(filename) | |
| if not issue_id: | |
| return None | |
| if "Convietuirea" in issue_id: | |
| return f"https://adt.arcanum.com/ro/view/{issue_id}" | |
| elif "GazetaMatematica" in issue_id: | |
| return f"https://adt.arcanum.com/en/view/{issue_id}" | |
| else: | |
| return f"https://adt.arcanum.com/ro/view/{issue_id}" | |
| def get_all_pdf_segments_for_issue(self, issue_url): | |
| """FIXED: Scanează toate fișierele PDF pentru un issue specific""" | |
| issue_id = issue_url.rstrip('/').split('/')[-1] | |
| segments = [] | |
| try: | |
| for filename in os.listdir(self.download_dir): | |
| if not filename.lower().endswith('.pdf'): | |
| continue | |
| file_issue_id = self.extract_issue_id_from_filename(filename) | |
| if file_issue_id == issue_id: | |
| # Extrage intervalul de pagini | |
| match = re.search(r'__pages(\d+)-(\d+)\.pdf', filename) | |
| if match: | |
| start_page = int(match.group(1)) | |
| end_page = int(match.group(2)) | |
| segments.append({ | |
| 'filename': filename, | |
| 'start': start_page, | |
| 'end': end_page, | |
| 'path': os.path.join(self.download_dir, filename) | |
| }) | |
| except Exception as e: | |
| print(f"⚠ Eroare la scanarea fișierelor pentru {issue_url}: {e}") | |
| # Sortează după pagina de început | |
| segments.sort(key=lambda x: x['start']) | |
| return segments | |
| def get_existing_pdf_segments(self, issue_url): | |
| """FIXED: Scanează toate segmentele existente și returnează ultima pagină""" | |
| segments = self.get_all_pdf_segments_for_issue(issue_url) | |
| if not segments: | |
| return 0 | |
| # Găsește cea mai mare pagină finală | |
| max_page = max(seg['end'] for seg in segments) | |
| print(f"📊 Fișiere PDF existente pentru {issue_url}:") | |
| for seg in segments: | |
| print(f" 📄 {seg['filename']} (pagini {seg['start']}-{seg['end']})") | |
| return max_page | |
| def calculate_expected_segments(self, total_pages): | |
| """ | |
| NOUĂ FUNCȚIE: Calculează toate segmentele așteptate bazat pe total_pages | |
| Returns: List of tuples (start_page, end_page) | |
| """ | |
| if not total_pages or total_pages <= 0: | |
| return [] | |
| bs = self.batch_size # 50 | |
| expected_segments = [] | |
| # Primul segment: 1 până la (bs-1), adică 1-49 | |
| first_end = min(bs - 1, total_pages) | |
| if first_end >= 1: | |
| expected_segments.append((1, first_end)) | |
| # Segmentele următoare: bs până la final | |
| current_start = bs | |
| while current_start < total_pages: | |
| current_end = min(current_start + bs - 1, total_pages) | |
| expected_segments.append((current_start, current_end)) | |
| current_start += bs | |
| return expected_segments | |
| def verify_physical_segments(self, issue_url, total_pages): | |
| """ | |
| NOUĂ FUNCȚIE CRITICĂ: Verifică că TOATE segmentele fizice există pe disk | |
| Returns: (is_complete, missing_segments, existing_segments) | |
| """ | |
| if not total_pages or total_pages <= 0: | |
| return False, [], [] | |
| # Calculează segmentele așteptate | |
| expected_segments = self.calculate_expected_segments(total_pages) | |
| # Obține segmentele existente pe disk | |
| existing_segments = self.get_all_pdf_segments_for_issue(issue_url) | |
| # Creează set-uri pentru comparație | |
| expected_set = set(expected_segments) | |
| existing_set = set((seg['start'], seg['end']) for seg in existing_segments) | |
| # Identifică segmentele lipsă | |
| missing_set = expected_set - existing_set | |
| missing_segments = sorted(list(missing_set)) | |
| is_complete = len(missing_segments) == 0 | |
| return is_complete, missing_segments, existing_segments | |
| def verify_and_report_missing_segments(self, issue_url, total_pages, item=None): | |
| """ | |
| NOUĂ FUNCȚIE: Verifică completitudinea și raportează segmentele lipsă | |
| Returns: True dacă colecția este completă, False dacă lipsesc segmente | |
| """ | |
| is_complete, missing_segments, existing_segments = self.verify_physical_segments(issue_url, total_pages) | |
| if is_complete: | |
| print(f"✅ VERIFICARE FIZICĂ: Toate {len(existing_segments)} segmente există pe disk") | |
| return True | |
| else: | |
| print(f"❌ VERIFICARE FIZICĂ: LIPSESC {len(missing_segments)} SEGMENTE!") | |
| print(f" 📊 Existente: {len(existing_segments)} segmente") | |
| print(f" 📊 Așteptate: {len(missing_segments) + len(existing_segments)} segmente") | |
| print(f" 🔍 Segmente LIPSĂ:") | |
| for start, end in missing_segments: | |
| print(f" ❌ pages{start}-{end}.pdf") | |
| # Dacă avem item din state.json, marchează-l ca incomplet | |
| if item: | |
| if item.get("completed_at"): | |
| print(f" 🔧 CORECTEZ: Șterg completed_at pentru a relua descărcarea") | |
| item["completed_at"] = "" | |
| if item.get("pages") == total_pages: | |
| print(f" 🔧 CORECTEZ: Resetez pages la 0 pentru reluare") | |
| item["pages"] = 0 | |
| return False | |
| def reconstruct_all_issues_from_disk(self): | |
| """FIXED: Reconstruiește complet progresul din fișierele de pe disk""" | |
| print("🔍 SCANEZ COMPLET toate fișierele PDF de pe disk...") | |
| # Grupează fișierele după issue ID | |
| issues_on_disk = {} | |
| try: | |
| for filename in os.listdir(self.download_dir): | |
| if not filename.lower().endswith('.pdf'): | |
| continue | |
| issue_id = self.extract_issue_id_from_filename(filename) | |
| if not issue_id: | |
| continue | |
| # Extrage intervalul de pagini | |
| match = re.search(r'__pages(\d+)-(\d+)\.pdf', filename) | |
| if not match: | |
| continue | |
| start_page = int(match.group(1)) | |
| end_page = int(match.group(2)) | |
| if issue_id not in issues_on_disk: | |
| issues_on_disk[issue_id] = { | |
| 'segments': [], | |
| 'max_page': 0, | |
| 'url': self.extract_issue_url_from_filename(filename) | |
| } | |
| issues_on_disk[issue_id]['segments'].append({ | |
| 'filename': filename, | |
| 'start': start_page, | |
| 'end': end_page | |
| }) | |
| if end_page > issues_on_disk[issue_id]['max_page']: | |
| issues_on_disk[issue_id]['max_page'] = end_page | |
| except Exception as e: | |
| print(f"⚠ Eroare la scanarea disk-ului: {e}") | |
| return {} | |
| # Afișează rezultatele scanării | |
| print(f"📊 Găsite {len(issues_on_disk)} issue-uri pe disk:") | |
| for issue_id, data in issues_on_disk.items(): | |
| segments_count = len(data['segments']) | |
| max_page = data['max_page'] | |
| url = data['url'] | |
| print(f" 📁 {issue_id}: {segments_count} segmente, max pagina {max_page}") | |
| print(f" 🔗 URL: {url}") | |
| # Afișează segmentele sortate | |
| data['segments'].sort(key=lambda x: x['start']) | |
| for seg in data['segments'][:3]: # Primele 3 | |
| print(f" 📄 {seg['filename']} ({seg['start']}-{seg['end']})") | |
| if segments_count > 3: | |
| print(f" 📄 ... și încă {segments_count - 3} segmente") | |
| return issues_on_disk | |
| def sync_json_with_disk_files(self): | |
| """SAFE: Îmbogățește informațiile din JSON cu cele de pe disk, ZERO pierderi + SORTARE CRONOLOGICĂ CORECTĂ""" | |
| print("🔄 MERGE SAFE - combinez informațiile din JSON cu cele de pe disk...") | |
| # PASUL 1: Scanează complet disk-ul | |
| issues_on_disk = self.reconstruct_all_issues_from_disk() | |
| # PASUL 2: PĂSTREAZĂ TOATE issue-urile existente din JSON (ZERO pierderi) | |
| existing_issues_by_url = {} | |
| for item in self.state.get("downloaded_issues", []): | |
| url = item.get("url", "").rstrip('/') | |
| existing_issues_by_url[url] = item.copy() # DEEP COPY pentru siguranță | |
| print(f"📋 PĂSTREZ {len(existing_issues_by_url)} issue-uri din JSON existent") | |
| # PASUL 3: MERGE cu datele de pe disk (doar îmbogățește, nu șterge) | |
| enriched_count = 0 | |
| new_from_disk_count = 0 | |
| for issue_id, disk_data in issues_on_disk.items(): | |
| url = disk_data['url'] | |
| if not url: | |
| continue | |
| max_page = disk_data['max_page'] | |
| segments_count = len(disk_data['segments']) | |
| is_complete = self.is_issue_complete_by_end_page(max_page) | |
| if url in existing_issues_by_url: | |
| # ÎMBOGĂȚEȘTE issue-ul existent (doar dacă progresul e mai mare) | |
| existing_issue = existing_issues_by_url[url] | |
| current_progress = existing_issue.get("last_successful_segment_end", 0) | |
| if max_page > current_progress: | |
| # ÎMBOGĂȚEȘTE doar câmpurile necesare, păstrează restul | |
| existing_issue["last_successful_segment_end"] = max_page | |
| if not existing_issue.get("total_pages"): | |
| existing_issue["total_pages"] = max_page | |
| enriched_count += 1 | |
| print(f"🔄 ÎMBOGĂȚIT: {url} - progres {current_progress} → {max_page}") | |
| # Marchează ca complet DOAR dacă nu era deja marcat | |
| if is_complete and not existing_issue.get("completed_at"): | |
| existing_issue["completed_at"] = datetime.now().isoformat(timespec="seconds") | |
| existing_issue["pages"] = max_page | |
| existing_issue["total_pages"] = max_page | |
| print(f"✅ MARCAT ca complet: {url} ({max_page} pagini)") | |
| else: | |
| # Issue complet nou găsit doar pe disk - ADAUGĂ | |
| new_issue = { | |
| "url": url, | |
| "title": issue_id.replace("-", " ").replace("_", " "), | |
| "subtitle": "", | |
| "pages": max_page if is_complete else 0, | |
| "completed_at": datetime.now().isoformat(timespec="seconds") if is_complete else "", | |
| "last_successful_segment_end": max_page, | |
| "total_pages": max_page if is_complete else None | |
| } | |
| existing_issues_by_url[url] = new_issue | |
| new_from_disk_count += 1 | |
| print(f"➕ ADĂUGAT nou din disk: {url} ({max_page} pagini, {segments_count} segmente)") | |
| # PASUL 4: Reconstruiește lista finală (TOATE issue-urile păstrate) | |
| all_issues_list = list(existing_issues_by_url.values()) | |
| # PASUL 5: SORTARE CRONOLOGICĂ CORECTĂ | |
| partial_issues = [] | |
| complete_issues = [] | |
| for issue in all_issues_list: | |
| is_partial = (issue.get("last_successful_segment_end", 0) > 0 and | |
| not issue.get("completed_at") and | |
| issue.get("total_pages") and | |
| issue.get("last_successful_segment_end", 0) < issue.get("total_pages", 0)) | |
| if is_partial: | |
| partial_issues.append(issue) | |
| print(f"🔄 Issue parțial: {issue['url']} ({issue.get('last_successful_segment_end', 0)}/{issue.get('total_pages', 0)} pagini)") | |
| else: | |
| complete_issues.append(issue) | |
| # SORTARE CRONOLOGICĂ PENTRU COMPLETE ISSUES | |
| # Sortează issue-urile complete după completed_at (cel mai recent primul) | |
| def sort_key_for_complete(issue): | |
| completed_at = issue.get("completed_at", "") | |
| if completed_at: | |
| try: | |
| # Convertește la datetime pentru sortare corectă | |
| return datetime.fromisoformat(completed_at.replace('Z', '+00:00')) | |
| except: | |
| return datetime.min | |
| else: | |
| # Issue-urile fără completed_at merg la sfârșit | |
| return datetime.min | |
| # Sortează: parțiale după progres (desc), complete după data (desc - cel mai recent primul) | |
| partial_issues.sort(key=lambda x: x.get("last_successful_segment_end", 0), reverse=True) | |
| complete_issues.sort(key=sort_key_for_complete, reverse=True) # Cel mai recent primul | |
| print(f"\n📊 SORTARE CRONOLOGICĂ APLICATĂ:") | |
| print(f" 🔄 Issue-uri parțiale: {len(partial_issues)} (sortate după progres)") | |
| if complete_issues: | |
| print(f" ✅ Issue-uri complete: {len(complete_issues)} (sortate cronologic)") | |
| print(f" 📅 Cel mai recent: {complete_issues[0].get('completed_at', 'N/A')}") | |
| print(f" 📅 Cel mai vechi: {complete_issues[-1].get('completed_at', 'N/A')}") | |
| # Afișează primele 5 pentru verificare | |
| print(f" 🔍 Ordinea cronologică (primele 5):") | |
| for i, issue in enumerate(complete_issues[:5]): | |
| url = issue.get('url', '').split('/')[-1] | |
| completed_at = issue.get('completed_at', 'N/A') | |
| print(f" {i+1}. {url} - {completed_at}") | |
| # PASUL 6: Actualizează starea SAFE (păstrează tot ce nu modificăm) | |
| original_count = self.state.get("count", 0) | |
| final_issues = partial_issues + complete_issues # Parțiale primul, apoi complete cronologic | |
| actual_complete_count = len([i for i in final_issues if i.get("completed_at")]) | |
| # PĂSTREAZĂ toate câmpurile existente, actualizează doar ce e necesar | |
| self.state["downloaded_issues"] = final_issues | |
| self.state["count"] = max(original_count, actual_complete_count) # Nu scade niciodată | |
| self._save_state_safe() | |
| print(f"✅ MERGE COMPLET cu SORTARE CRONOLOGICĂ CORECTĂ - ZERO pierderi:") | |
| print(f" 📊 Total issues: {len(final_issues)} (înainte: {len(existing_issues_by_url) - new_from_disk_count})") | |
| print(f" 🔄 Îmbogățite: {enriched_count}") | |
| print(f" ➕ Adăugate din disk: {new_from_disk_count}") | |
| print(f" 🔄 Parțiale: {len(partial_issues)}") | |
| print(f" ✅ Complete: {len(complete_issues)}") | |
| print(f" 🎯 Count păstrat/actualizat: {original_count} → {self.state['count']}") | |
| if partial_issues: | |
| print("🎯 Issue-urile parțiale vor fi procesate primele!") | |
| print("📅 Issue-urile complete sunt acum sortate cronologic (cel mai recent primul)!") | |
| def process_completed_but_unfinalized_issues(self): | |
| """ | |
| Procesează issues care sunt complet descărcate dar nu au fost finalizate | |
| (au last_successful_segment_end == total_pages dar pages=0 și completed_at="") | |
| """ | |
| print("\n🔍 VERIFICARE: Caut issues complet descărcate dar nefinalizate...") | |
| issues_to_finalize = [] | |
| for issue in self.state.get("downloaded_issues", []): | |
| url = issue.get("url", "") | |
| last_segment = issue.get("last_successful_segment_end", 0) | |
| total_pages = issue.get("total_pages", 0) | |
| completed_at = issue.get("completed_at", "") | |
| pages = issue.get("pages", 0) | |
| title = issue.get("title", "") | |
| subtitle = issue.get("subtitle", "") | |
| # Verifică dacă e complet descărcat dar nefinalizat | |
| if (last_segment > 0 and | |
| total_pages > 0 and | |
| last_segment >= total_pages and | |
| not completed_at and | |
| pages == 0): | |
| issues_to_finalize.append({ | |
| 'url': url, | |
| 'title': title, | |
| 'subtitle': subtitle, | |
| 'last_segment': last_segment, | |
| 'total_pages': total_pages | |
| }) | |
| print(f"\n📋 GĂSIT issue nefinalizat: {url}") | |
| print(f" 📊 Progres: {last_segment}/{total_pages} (100% descărcat)") | |
| print(f" ❌ Status: pages=0, completed_at=empty") | |
| if not issues_to_finalize: | |
| print("✅ Nu am găsit issues nefinalizate - toate sunt OK!") | |
| return | |
| print(f"\n🎯 FINALIZARE AUTOMATĂ: {len(issues_to_finalize)} issues vor fi procesate...") | |
| for issue_data in issues_to_finalize: | |
| url = issue_data['url'] | |
| title = issue_data['title'] | |
| subtitle = issue_data['subtitle'] | |
| total_pages = issue_data['total_pages'] | |
| try: | |
| print(f"\n{'='*60}") | |
| print(f"🔄 PROCESEZ: {url}") | |
| print(f"{'='*60}") | |
| # VERIFICARE CRITICĂ: Verifică că TOATE segmentele fizice există pe disk | |
| print(f"🔍 VERIFICARE FIZICĂ: Verific că toate segmentele există pe disk...") | |
| is_physically_complete = self.verify_and_report_missing_segments(url, total_pages) | |
| if not is_physically_complete: | |
| print(f"⚠ SKIP: Colecția NU este completă pe disk - lipsesc segmente!") | |
| print(f" 🔄 Issue-ul va fi reluat pentru a descărca segmentele lipsă") | |
| # Găsește issue-ul în state și marchează-l ca incomplet | |
| for state_issue in self.state.get("downloaded_issues", []): | |
| if state_issue.get("url") == url: | |
| state_issue["completed_at"] = "" | |
| state_issue["pages"] = 0 | |
| state_issue["last_successful_segment_end"] = 0 | |
| self._save_state_safe() | |
| print(f" ✅ Issue resetat în state.json pentru reluare") | |
| break | |
| continue | |
| # Verifică din nou pe disk că toate fișierele sunt prezente | |
| final_segments = self.get_all_pdf_segments_for_issue(url) | |
| if not final_segments: | |
| print(f"⚠ SKIP: Nu am găsit fișiere PDF pe disk pentru {url}") | |
| continue | |
| # Calculează progresul real | |
| real_final_page = max(seg['end'] for seg in final_segments) | |
| real_completion_percent = (real_final_page / total_pages) * 100 | |
| print(f"📊 Verificare disk: {real_final_page}/{total_pages} ({real_completion_percent:.1f}%)") | |
| if real_completion_percent < 95: | |
| print(f"⚠ SKIP: Progresul real < 95% - nu finalizez") | |
| continue | |
| # PASUL 1: Marchează ca terminat în JSON | |
| print(f"📝 Marchez ca terminat în JSON...") | |
| self.mark_issue_done(url, real_final_page, title=title, subtitle=subtitle, total_pages=total_pages) | |
| print(f"✅ Marcat ca terminat în JSON") | |
| # Așteaptă salvarea JSON | |
| time.sleep(2) | |
| # PASUL 2: Procesează PDF-urile (backup + merge + mutare) | |
| print(f"📦 Procesez PDF-urile (backup + merge + mutare)...") | |
| self.copy_and_combine_issue_pdfs(url, title or url.split('/')[-1]) | |
| print(f"✅ PDF-urile procesate cu succes!") | |
| # Așteaptă finalizarea procesării | |
| time.sleep(5) | |
| print(f"{'='*60}") | |
| print(f"🎉 FINALIZAT CU SUCCES: {url}") | |
| print(f"{'='*60}\n") | |
| except Exception as e: | |
| print(f"❌ EROARE la finalizarea {url}: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| continue | |
| print(f"\n✅ FINALIZARE AUTOMATĂ COMPLETĂ: {len(issues_to_finalize)} issues procesate") | |
| def cleanup_duplicate_issues(self): | |
| """NOUĂ FUNCȚIE: Elimină dublurile din state.json""" | |
| print("🧹 CURĂȚARE: Verific și elimin dublurile din state.json...") | |
| issues = self.state.get("downloaded_issues", []) | |
| if not issues: | |
| return | |
| # Grupează după URL normalizat | |
| url_groups = {} | |
| for i, item in enumerate(issues): | |
| url = item.get("url", "").rstrip('/').lower() | |
| if not url: | |
| continue | |
| if url not in url_groups: | |
| url_groups[url] = [] | |
| url_groups[url].append((i, item)) | |
| # Găsește și rezolvă dublurile | |
| duplicates_found = 0 | |
| clean_issues = [] | |
| processed_urls = set() | |
| for original_url, group in url_groups.items(): | |
| if len(group) > 1: | |
| duplicates_found += 1 | |
| print(f"🔍 DUBLURĂ găsită pentru {original_url}: {len(group)} intrări") | |
| # Găsește cea mai completă versiune | |
| best_item = None | |
| best_score = -1 | |
| for idx, item in group: | |
| score = 0 | |
| if item.get("completed_at"): score += 100 | |
| if item.get("total_pages"): score += 50 | |
| if item.get("title"): score += 10 | |
| if item.get("last_successful_segment_end", 0) > 0: score += 20 | |
| print(f" 📊 Index {idx}: score {score}, completed: {bool(item.get('completed_at'))}") | |
| if score > best_score: | |
| best_score = score | |
| best_item = item | |
| print(f" ✅ Păstrez cea mai completă versiune (score: {best_score})") | |
| clean_issues.append(best_item) | |
| else: | |
| # Nu e dublură, păstrează-l | |
| clean_issues.append(group[0][1]) | |
| processed_urls.add(original_url) | |
| if duplicates_found > 0: | |
| print(f"🧹 ELIMINAT {duplicates_found} dubluri din {len(issues)} issues") | |
| print(f"📊 Rămas cu {len(clean_issues)} issues unice") | |
| self.state["downloaded_issues"] = clean_issues | |
| self._save_state_safe() | |
| else: | |
| print("✅ Nu am găsit dubluri în state.json") | |
| def is_issue_really_complete(self, item, verify_physical=True): | |
| """ | |
| FIXED: Nu verifica fizic issue-urile deja procesate complet | |
| """ | |
| completed_at = item.get("completed_at") | |
| last_segment = item.get("last_successful_segment_end", 0) | |
| total_pages = item.get("total_pages") | |
| pages = item.get("pages", 0) | |
| url = item.get("url", "") | |
| # VERIFICARE 1: State.json verificare standard | |
| json_complete = ( | |
| completed_at and | |
| total_pages and | |
| total_pages > 0 and | |
| last_segment >= total_pages and | |
| pages > 0 | |
| ) | |
| if not json_complete: | |
| return False | |
| # ✅ FIX CRUCIAL: Dacă pages == total_pages, issue-ul e PROCESAT! | |
| # PDF-ul final există, segmentele au fost șterse | |
| # NU mai verificăm fizic pe disk! | |
| if pages == total_pages: | |
| return True | |
| # VERIFICARE 2: Verificare FIZICĂ - DOAR pentru issues parțiale | |
| if verify_physical and total_pages and total_pages > 0: | |
| is_physically_complete, missing_segments, _ = self.verify_physical_segments(url, total_pages) | |
| if not is_physically_complete: | |
| print(f"⚠️ ATENȚIE: {url}") | |
| print(f" ✅ În state.json: marcat COMPLET") | |
| print(f" ❌ Pe disk: LIPSESC {len(missing_segments)} segmente!") | |
| return False | |
| return True | |
| def fix_incorrectly_marked_complete_issues(self): | |
| """ | |
| FIXED: Nu resetează issue-urile deja procesate complet | |
| """ | |
| print("🔧 CORECTEZ issue-urile marcate GREȘIT ca complete...") | |
| # ⚡ VERIFICARE PRIORITATE: Există issues incomplete de procesat? | |
| incomplete_issues_exist = False | |
| for item in self.state.get("downloaded_issues", []): | |
| if (item.get("pages", 0) == 0 and | |
| not item.get("completed_at") and | |
| item.get("total_pages", 0) > 0): | |
| incomplete_issues_exist = True | |
| break | |
| if incomplete_issues_exist: | |
| print("⚡ PRIORITATE: Există issues incomplete de procesat") | |
| print(" ⏭️ SKIP verificarea fizică a issues complete (CAZUL 3)") | |
| print(" ✅ Focusez pe finalizarea issues incomplete mai întâi!") | |
| fixes_applied = 0 | |
| for item in self.state.get("downloaded_issues", []): | |
| completed_at = item.get("completed_at") | |
| last_segment = item.get("last_successful_segment_end", 0) | |
| total_pages = item.get("total_pages") | |
| pages = item.get("pages", 0) | |
| url = item.get("url", "") | |
| # ✅ FIX CRUCIAL: SKIP issue-uri PROCESATE complet | |
| # Dacă pages == total_pages, PDF-ul final există, segmentele au fost șterse | |
| if completed_at and pages > 0 and total_pages and pages == total_pages: | |
| # Issue PROCESAT complet - NU verificăm fizic! | |
| continue | |
| # CAZUL 1 & 2: Verificare state.json standard | |
| if (completed_at and | |
| pages == 0 and | |
| total_pages and | |
| last_segment < total_pages): | |
| print(f"🚨 CORECTEZ issue marcat GREȘIT ca complet: {url}") | |
| item["completed_at"] = "" | |
| item["pages"] = 0 | |
| fixes_applied += 1 | |
| continue | |
| # CAZUL 3: Verificare FIZICĂ - DOAR pentru issues NEPROCESSATE | |
| # (completed_at există dar pages == 0 sau pages < total_pages) | |
| if (completed_at and | |
| total_pages and | |
| total_pages > 0 and | |
| pages < total_pages and # ✅ NU verifică dacă pages == total_pages | |
| not incomplete_issues_exist): | |
| is_physically_complete = self.verify_and_report_missing_segments(url, total_pages, item) | |
| if not is_physically_complete: | |
| print(f"🚨 CORECTEZ issue marcat complet în JSON dar INCOMPLET pe disk: {url}") | |
| item["completed_at"] = "" | |
| item["pages"] = 0 | |
| fixes_applied += 1 | |
| if fixes_applied > 0: | |
| print(f"🔧 CORECTAT {fixes_applied} issue-uri marcate greșit ca complete") | |
| self._save_state_safe() | |
| self._save_skip_urls() | |
| else: | |
| print("✅ Nu am găsit issue-uri marcate greșit ca complete") | |
| return fixes_applied | |
| def fix_progress_based_on_disk(self): | |
| """NOUĂ FUNCȚIE: Corectează last_successful_segment_end bazat pe ce există EFECTIV pe disk | |
| PROTECTED: Protecție împotriva resetărilor masive dacă disk-ul e gol | |
| """ | |
| print("🔍 SCANEZ disk-ul și corectez progresul în JSON...") | |
| # 🛡️ PROTECȚIE: Verifică dacă disk-ul are CEVA fișiere PDF | |
| # Dacă disk-ul e complet gol sau aproape gol, NU reseta nimic! | |
| try: | |
| pdf_files_on_disk = [f for f in os.listdir(self.download_dir) if f.lower().endswith('.pdf')] | |
| pdf_count = len(pdf_files_on_disk) | |
| if pdf_count < 10: | |
| print(f"\n{'='*70}") | |
| print(f"🚨 ATENȚIE: PROTECȚIE DISK GOL ACTIVATĂ!") | |
| print(f"{'='*70}") | |
| print(f"⚠️ Disk-ul are doar {pdf_count} fișiere PDF.") | |
| print(f" Acesta pare a fi prea puțin comparativ cu issues din state.json.") | |
| print(f" POATE fișierele au fost mutate/șterse temporar?") | |
| print(f"\n🛡️ PROTECȚIE: NU voi reseta progresul pentru a preveni pierderea datelor!") | |
| print(f" Verifică dacă fișierele PDF există pe disk și încearcă din nou.") | |
| print(f"{'='*70}\n") | |
| return # NU continua! | |
| except Exception as e: | |
| print(f"⚠️ Nu am putut verifica disk-ul: {e}") | |
| print(f" Pentru siguranță, NU voi modifica progresul.") | |
| return | |
| corrections = 0 | |
| resets_to_zero = 0 # Contorizează câte issues vor fi resetate la 0 | |
| for item in self.state.get("downloaded_issues", []): | |
| url = item.get("url", "") | |
| if not url: | |
| continue | |
| # Obține progresul din JSON | |
| json_progress = item.get("last_successful_segment_end", 0) | |
| total_pages = item.get("total_pages", 0) | |
| completed_at = item.get("completed_at", "") | |
| pages = item.get("pages", 0) | |
| # ⭐ IMPORTANT: SKIP issue-urile deja finalizate complet! | |
| # După procesare, segmentele individuale sunt șterse/mutate, rămâne doar PDF-ul final | |
| # DAR: Verificăm dacă PDF-ul final combinat există efectiv pe disk! | |
| if completed_at and pages > 0 and pages == total_pages: | |
| # Verifică dacă există PDF-ul FINAL combinat (nu segmente) | |
| try: | |
| # Extrage ID-ul din URL (ex: Farmacia_1972-1673578372) | |
| normalized_url = self._normalize_url(url) | |
| issue_identifier = None | |
| # Caută fișiere care NU sunt segmente (fără __pages în nume) | |
| for filename in os.listdir(self.download_dir): | |
| if filename.lower().endswith('.pdf') and '__pages' not in filename: | |
| # Acest e un PDF final combinat | |
| if normalized_url.lower() in filename.lower(): | |
| issue_identifier = filename | |
| break | |
| if issue_identifier: | |
| # Există PDF-ul final combinat - Issue complet și procesat - NU îl atingem! | |
| continue | |
| else: | |
| # NU există PDF final, doar segmente - TREBUIE verificat! | |
| print(f"\n⚠️ ATENȚIE: {url} marcat complet dar lipsește PDF-ul final!") | |
| print(f" Verific segmentele individuale...") | |
| except Exception as e: | |
| # Eroare la verificare - mai bine verificăm segmentele | |
| pass | |
| # Scanează disk-ul pentru acest issue (doar pentru parțiale/incomplete) | |
| actual_segments = self.get_all_pdf_segments_for_issue(url) | |
| if actual_segments: | |
| # === CALCUL CORECT: Găsește progresul REAL CONSECUTIV de pe disk === | |
| # Nu folosim max(seg['end']), ci găsim ultimul segment consecutiv de la pagina 1 | |
| actual_segments.sort(key=lambda x: x['start']) | |
| # Calculează segmentele așteptate | |
| bs = self.batch_size # 50 | |
| all_segments_expected = [] | |
| # Primul segment: 1-49 | |
| if total_pages > 0: | |
| first_end = min(bs - 1, total_pages) | |
| if first_end >= 1: | |
| all_segments_expected.append((1, first_end)) | |
| # Segmentele următoare: 50-99, 100-149, etc. | |
| current = bs | |
| while current <= total_pages: | |
| end = min(current + bs - 1, total_pages) | |
| all_segments_expected.append((current, end)) | |
| current += bs | |
| # Găsește ultimul segment consecutiv de la început | |
| real_progress = 0 | |
| for expected_start, expected_end in all_segments_expected: | |
| found = False | |
| for disk_seg in actual_segments: | |
| if disk_seg['start'] <= expected_start and disk_seg['end'] >= expected_end: | |
| found = True | |
| real_progress = expected_end # Actualizează progresul | |
| break | |
| # Dacă lipsește un segment, OPREȘTE | |
| if not found: | |
| break | |
| # Dacă progresul din JSON diferă de cel real | |
| if json_progress != real_progress: | |
| print(f"\n⚠️ DISCREPANȚĂ pentru {url}:") | |
| print(f" JSON zicea: {json_progress} pagini") | |
| print(f" Disk-ul arată CONSECUTIV: {real_progress} pagini") | |
| print(f" Segmente pe disk: {len(actual_segments)}") | |
| # Afișează segmentele găsite | |
| for seg in sorted(actual_segments, key=lambda x: x['start'])[:5]: | |
| print(f" 📄 {seg['filename']} ({seg['start']}-{seg['end']})") | |
| # CORECTEAZĂ cu progresul real CONSECUTIV | |
| item["last_successful_segment_end"] = real_progress | |
| # Dacă era marcat ca terminat dar nu e complet, demarchez | |
| if completed_at and real_progress < total_pages: | |
| print(f" 🔄 DEMARCHEZ ca terminat - progres incomplet!") | |
| item["completed_at"] = "" | |
| item["pages"] = 0 | |
| corrections += 1 | |
| print(f" ✅ CORECTAT: {json_progress} → {real_progress} (CONSECUTIV)") | |
| elif json_progress > 0 and not completed_at: | |
| # JSON arată progres dar disk-ul e gol - DAR DOAR pentru issue-uri NEFINALIZATE! | |
| print(f"\n🚨 PROBLEMĂ GRAVĂ pentru {url}:") | |
| print(f" JSON arată {json_progress} pagini, dar disk-ul e GOL!") | |
| print(f" 🔄 RESETEZ progresul la 0") | |
| item["last_successful_segment_end"] = 0 | |
| item["completed_at"] = "" | |
| item["pages"] = 0 | |
| corrections += 1 | |
| resets_to_zero += 1 | |
| # 🛡️ PROTECȚIE FINALĂ: Nu permite resetări masive | |
| if resets_to_zero > 20: | |
| print(f"\n{'='*70}") | |
| print(f"🚨 ALERTĂ CRITICĂ: PROTECȚIE RESETĂRI MASIVE ACTIVATĂ!") | |
| print(f"{'='*70}") | |
| print(f"❌ Funcția fix_progress_based_on_disk() vrea să reseteze {resets_to_zero} issues la 0!") | |
| print(f" Acesta pare a fi un număr suspect de mare.") | |
| print(f" POATE fișierele PDF au fost mutate temporar sau disk-ul e inaccesibil?") | |
| print(f"\n🛡️ PROTECȚIE: NU voi salva aceste modificări pentru a preveni pierderea datelor!") | |
| print(f" Verifică că fișierele PDF există pe disk și încearcă din nou.") | |
| print(f"{'='*70}\n") | |
| return # NU salva! | |
| if corrections > 0: | |
| print(f"\n✅ CORECTAT progresul pentru {corrections} issues") | |
| if resets_to_zero > 0: | |
| print(f" ⚠️ Dintre care {resets_to_zero} au fost resetate la 0 (disk gol)") | |
| self._save_state_safe() | |
| self._save_skip_urls() | |
| else: | |
| print("✅ Progresul din JSON corespunde cu disk-ul") | |
| def get_pending_partial_issues(self): | |
| """IMPROVED: Găsește TOATE issue-urile parțiale din TOATE colecțiile""" | |
| pending_partials = [] | |
| for item in self.state.get("downloaded_issues", []): | |
| url = item.get("url", "").rstrip('/') | |
| last_segment = item.get("last_successful_segment_end", 0) | |
| total_pages = item.get("total_pages") | |
| completed_at = item.get("completed_at", "") | |
| pages = item.get("pages", 0) | |
| # Skip URL-urile complet descărcate | |
| if url in self.dynamic_skip_urls: | |
| continue | |
| # CONDIȚIE PRECISĂ pentru parțiale | |
| is_partial = ( | |
| last_segment > 0 and # Are progres | |
| total_pages and total_pages > 0 and # Are total valid | |
| last_segment < total_pages and # Nu e complet | |
| not completed_at and # Nu e marcat terminat | |
| pages == 0 # NU e finalizat (pages = 0) | |
| ) | |
| if is_partial: | |
| completion_percent = (last_segment / total_pages) * 100 | |
| item_with_priority = item.copy() | |
| item_with_priority['completion_percent'] = completion_percent | |
| item_with_priority['remaining_pages'] = total_pages - last_segment | |
| pending_partials.append(item_with_priority) | |
| print(f"🔄 PARȚIAL: {url}") | |
| print(f" Progres: {last_segment}/{total_pages} ({completion_percent:.1f}%)") | |
| print(f" Rămân: {total_pages - last_segment} pagini") | |
| # SORTARE: prioritizează după completitudine (aproape finalizate = prioritate) | |
| pending_partials.sort(key=lambda x: x['completion_percent'], reverse=True) | |
| if pending_partials: | |
| print(f"\n📋 ORDINEA DE PROCESARE ({len(pending_partials)} parțiale):") | |
| for i, item in enumerate(pending_partials[:10]): # Primele 10 | |
| url_short = item['url'].split('/')[-1] | |
| percent = item['completion_percent'] | |
| remaining = item['remaining_pages'] | |
| print(f" {i+1}. {url_short}: {percent:.1f}% complet, {remaining} pagini") | |
| if len(pending_partials) > 10: | |
| print(f" ... și încă {len(pending_partials) - 10} parțiale") | |
| return pending_partials | |
| def _normalize_downloaded_issues(self, raw): | |
| normalized = [] | |
| for item in raw: | |
| if isinstance(item, str): | |
| normalized.append({ | |
| "url": item.rstrip('/'), | |
| "title": "", | |
| "subtitle": "", | |
| "pages": 0, | |
| "completed_at": "", | |
| "last_successful_segment_end": 0, | |
| "total_pages": None | |
| }) | |
| elif isinstance(item, dict): | |
| normalized.append({ | |
| "url": item.get("url", "").rstrip('/'), | |
| "title": item.get("title", ""), | |
| "subtitle": item.get("subtitle", ""), | |
| "pages": item.get("pages", 0), | |
| "completed_at": item.get("completed_at", ""), | |
| "last_successful_segment_end": item.get("last_successful_segment_end", 0), | |
| "total_pages": item.get("total_pages") | |
| }) | |
| return normalized | |
| def _repair_json_missing_comma(self, file_path): | |
| """ | |
| Repară JSON-ul când lipsește virgula după câmpul 'pages' | |
| Pattern: "pages": <număr>\n "completed_at" → "pages": <număr>,\n "completed_at" | |
| """ | |
| try: | |
| with open(file_path, "r", encoding="utf-8") as f: | |
| content = f.read() | |
| # Detectează pattern-ul: "pages": număr urmat direct de "completed_at" (fără virgulă) | |
| import re | |
| pattern = r'("pages"\s*:\s*\d+)\s*\n(\s*"completed_at")' | |
| # Verifică dacă există problema | |
| if re.search(pattern, content): | |
| print(f"🔧 REPARARE JSON: Detectată virgulă lipsă după 'pages' în {file_path}") | |
| # Adaugă virgula lipsă | |
| fixed_content = re.sub(pattern, r'\1,\n\2', content) | |
| # Salvează fișierul reparat | |
| with open(file_path, "w", encoding="utf-8") as f: | |
| f.write(fixed_content) | |
| print(f"✅ JSON reparat automat: virgulă adăugată după 'pages'") | |
| return True | |
| return False | |
| except Exception as e: | |
| print(f"⚠ Eroare la repararea JSON: {e}") | |
| return False | |
| def _load_state(self): | |
| """ULTRA SAFE: Nu șterge NICIODATĂ datele existente""" | |
| today = datetime.now().strftime("%Y-%m-%d") | |
| if os.path.exists(self.state_path): | |
| try: | |
| # REPARĂ JSON-ul dacă are virgulă lipsă după 'pages' | |
| self._repair_json_missing_comma(self.state_path) | |
| with open(self.state_path, "r", encoding="utf-8") as f: | |
| loaded = json.load(f) | |
| loaded = self._decode_unicode_escapes(loaded) | |
| # PĂSTREAZĂ TOATE issue-urile existente - ZERO ȘTERS | |
| existing_issues = self._normalize_downloaded_issues(loaded.get("downloaded_issues", [])) | |
| print(f"📋 ÎNCĂRCAT {len(existing_issues)} issue-uri din state.json") | |
| # Găsește issue-urile parțiale | |
| partial_issues = [] | |
| for issue in existing_issues: | |
| last_segment = issue.get("last_successful_segment_end", 0) | |
| total_pages = issue.get("total_pages") | |
| completed_at = issue.get("completed_at", "") | |
| if (last_segment > 0 and not completed_at and total_pages and last_segment < total_pages): | |
| partial_issues.append(issue) | |
| print(f"🔄 PARȚIAL: {issue['url']} - {last_segment}/{total_pages} pagini") | |
| complete_count = len([i for i in existing_issues if i.get("completed_at")]) | |
| # PĂSTREAZĂ TOT - doar actualizează data | |
| self.state = { | |
| "date": today, | |
| "count": loaded.get("count", complete_count), | |
| "downloaded_issues": existing_issues, # TOATE PĂSTRATE | |
| "pages_downloaded": loaded.get("pages_downloaded", 0), | |
| "recent_links": loaded.get("recent_links", []), | |
| "daily_limit_hit": False, | |
| "main_collection_completed": loaded.get("main_collection_completed", False), | |
| "current_additional_collection_index": loaded.get("current_additional_collection_index", 0) | |
| } | |
| print(f"✅ PĂSTRAT TOT: {complete_count} complete, {len(partial_issues)} parțiale") | |
| except Exception as e: | |
| print(f"❌ JSON CORRUPT: {e}") | |
| print(f"🛠️ RECUPEREZ din backup sau disk...") | |
| # Încearcă backup | |
| backup_path = self.state_path + ".backup" | |
| if os.path.exists(backup_path): | |
| print(f"🔄 Restabilesc din backup...") | |
| # REPARĂ backup-ul înainte de a-l copia | |
| print(f"🔧 Verific și repar backup-ul dacă e necesar...") | |
| self._repair_json_missing_comma(backup_path) | |
| shutil.copy2(backup_path, self.state_path) | |
| return self._load_state() # Recursiv cu backup | |
| # Altfel începe gol dar SCANEAZĂ DISK-UL | |
| print(f"🔍 SCANEZ DISK-UL pentru recuperare...") | |
| self.state = { | |
| "date": today, | |
| "count": 0, | |
| "downloaded_issues": [], | |
| "pages_downloaded": 0, | |
| "recent_links": [], | |
| "daily_limit_hit": False, | |
| "main_collection_completed": False, | |
| "current_additional_collection_index": 0 | |
| } | |
| else: | |
| print(f"📄 Nu există state.json") | |
| self.state = { | |
| "date": today, | |
| "count": 0, | |
| "downloaded_issues": [], | |
| "pages_downloaded": 0, | |
| "recent_links": [], | |
| "daily_limit_hit": False, | |
| "main_collection_completed": False, | |
| "current_additional_collection_index": 0 | |
| } | |
| self._save_state() | |
| def _create_daily_backup(self): | |
| """Creează backup zilnic al state.json (o singură dată pe zi)""" | |
| if not os.path.exists(self.state_path): | |
| print("📄 Nu există state.json pentru backup") | |
| return | |
| today = datetime.now().strftime("%Y-%m-%d") | |
| backup_path = self.state_path + ".backup" | |
| # Verifică dacă backup-ul există și dacă e din ziua de azi | |
| backup_is_today = False | |
| if os.path.exists(backup_path): | |
| backup_time = datetime.fromtimestamp(os.path.getmtime(backup_path)) | |
| backup_date = backup_time.strftime("%Y-%m-%d") | |
| if backup_date == today: | |
| backup_is_today = True | |
| print(f"✅ Backup zilnic deja existent pentru {today}") | |
| # Creează backup doar dacă nu există sau e din altă zi | |
| if not backup_is_today: | |
| try: | |
| shutil.copy2(self.state_path, backup_path) | |
| print(f"💾 BACKUP ZILNIC creat: {backup_path}") | |
| print(f"📅 Data backup: {today}") | |
| except Exception as e: | |
| print(f"⚠ Nu am putut crea backup zilnic: {e}") | |
| def _log_completed_issue(self, issue_url, title, subtitle, pages_count): | |
| """Înregistrează în log zilnic issue-urile finalizate""" | |
| today = datetime.now().strftime("%Y-%m-%d") | |
| current_time = datetime.now().strftime("%H:%M:%S") | |
| log_file = os.path.join(self.daily_log_dir, f"completed_{today}.log") | |
| try: | |
| # Verifică dacă issue-ul e deja în log-ul de azi | |
| issue_already_logged = False | |
| if os.path.exists(log_file): | |
| with open(log_file, "r", encoding="utf-8") as f: | |
| content = f.read() | |
| if issue_url in content: | |
| issue_already_logged = True | |
| if not issue_already_logged: | |
| with open(log_file, "a", encoding="utf-8") as f: | |
| f.write(f"\n{'='*80}\n") | |
| f.write(f"⏰ Ora finalizării: {current_time}\n") | |
| f.write(f"📋 URL: {issue_url}\n") | |
| f.write(f"📖 Titlu: {title}\n") | |
| if subtitle: | |
| f.write(f"📑 Subtitlu: {subtitle}\n") | |
| f.write(f"📄 Pagini: {pages_count}\n") | |
| f.write(f"{'='*80}\n") | |
| print(f"📝 Log zilnic actualizat: {log_file}") | |
| except Exception as e: | |
| print(f"⚠ Nu am putut scrie în log zilnic: {e}") | |
| def _log_state_changes(self, old_state, new_state, caller_function="Unknown"): | |
| """Loghează modificările făcute în state.json pentru debugging""" | |
| try: | |
| log_dir = os.path.join(os.path.dirname(self.state_path), "State_Change_Logs") | |
| os.makedirs(log_dir, exist_ok=True) | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| log_file = os.path.join(log_dir, f"state_changes_{timestamp}.log") | |
| with open(log_file, "w", encoding="utf-8") as f: | |
| f.write(f"{'='*70}\n") | |
| f.write(f"STATE.JSON CHANGE LOG\n") | |
| f.write(f"{'='*70}\n") | |
| f.write(f"Timestamp: {datetime.now().isoformat()}\n") | |
| f.write(f"Called by: {caller_function}\n") | |
| f.write(f"{'='*70}\n\n") | |
| # Compară numărul total de issues | |
| old_count = len(old_state.get("downloaded_issues", [])) | |
| new_count = len(new_state.get("downloaded_issues", [])) | |
| f.write(f"Total issues: {old_count} → {new_count}\n\n") | |
| # Detectează modificări masive suspecte (ALERTĂ!) | |
| pages_reset_count = 0 | |
| completed_at_reset_count = 0 | |
| old_issues = {item.get("url"): item for item in old_state.get("downloaded_issues", [])} | |
| new_issues = {item.get("url"): item for item in new_state.get("downloaded_issues", [])} | |
| for url, old_item in old_issues.items(): | |
| if url in new_issues: | |
| new_item = new_issues[url] | |
| old_pages = old_item.get("pages", 0) | |
| new_pages = new_item.get("pages", 0) | |
| old_completed = old_item.get("completed_at", "") | |
| new_completed = new_item.get("completed_at", "") | |
| # Detectează resetări | |
| if old_pages > 0 and new_pages == 0: | |
| pages_reset_count += 1 | |
| if old_completed and not new_completed: | |
| completed_at_reset_count += 1 | |
| # ALERTĂ MODIFICĂRI MASIVE | |
| if pages_reset_count > 10 or completed_at_reset_count > 10: | |
| f.write(f"🚨 ALERTĂ: MODIFICARE MASIVĂ DETECTATĂ!\n") | |
| f.write(f" - Issues cu pages resetat la 0: {pages_reset_count}\n") | |
| f.write(f" - Issues cu completed_at șters: {completed_at_reset_count}\n") | |
| f.write(f" - Funcție responsabilă: {caller_function}\n") | |
| f.write(f"{'='*70}\n\n") | |
| # Înregistrează modificările detaliate | |
| f.write(f"MODIFICĂRI DETECTATE:\n") | |
| f.write(f"{'='*70}\n\n") | |
| changes_found = False | |
| for url, old_item in old_issues.items(): | |
| if url in new_issues: | |
| new_item = new_issues[url] | |
| changes = [] | |
| # Verifică fiecare câmp important | |
| for key in ["pages", "completed_at", "last_successful_segment_end", "total_pages"]: | |
| old_val = old_item.get(key) | |
| new_val = new_item.get(key) | |
| if old_val != new_val: | |
| changes.append(f" {key}: {old_val} → {new_val}") | |
| if changes: | |
| changes_found = True | |
| f.write(f"URL: {url}\n") | |
| f.write(f" Title: {old_item.get('title', 'N/A')}\n") | |
| for change in changes: | |
| f.write(f"{change}\n") | |
| f.write(f"\n") | |
| if not changes_found: | |
| f.write("Nu s-au detectat modificări în issues existente.\n") | |
| # Înregistrează issues noi | |
| new_urls = set(new_issues.keys()) - set(old_issues.keys()) | |
| if new_urls: | |
| f.write(f"\n{'='*70}\n") | |
| f.write(f"ISSUES NOI ADĂUGATE: {len(new_urls)}\n") | |
| f.write(f"{'='*70}\n\n") | |
| for url in new_urls: | |
| item = new_issues[url] | |
| f.write(f"URL: {url}\n") | |
| f.write(f" Title: {item.get('title', 'N/A')}\n") | |
| f.write(f" Pages: {item.get('pages', 0)}\n") | |
| f.write(f" Progress: {item.get('last_successful_segment_end', 0)}/{item.get('total_pages', 0)}\n\n") | |
| # Păstrează doar ultimele 50 de log-uri | |
| log_files = sorted(os.listdir(log_dir)) | |
| if len(log_files) > 50: | |
| for old_log in log_files[:-50]: | |
| os.remove(os.path.join(log_dir, old_log)) | |
| except Exception as e: | |
| print(f"⚠ Nu am putut crea log pentru modificări: {e}") | |
| def _save_state_safe(self): | |
| """SAFE: Salvează starea cu backup timestamped și logging detaliat""" | |
| try: | |
| # PASUL 1: Citește starea VECHE pentru comparație | |
| old_state = {} | |
| if os.path.exists(self.state_path): | |
| try: | |
| with open(self.state_path, "r", encoding="utf-8") as f: | |
| old_state = json.load(f) | |
| except: | |
| old_state = {} | |
| # PASUL 2: PROTECȚIE ÎMPOTRIVA RESETĂRILOR MASIVE | |
| # Verifică dacă se încearcă resetarea masivă a pages la 0 | |
| if old_state.get("downloaded_issues"): | |
| old_completed_count = sum(1 for item in old_state.get("downloaded_issues", []) | |
| if item.get("pages", 0) > 0 and item.get("completed_at")) | |
| new_completed_count = sum(1 for item in self.state.get("downloaded_issues", []) | |
| if item.get("pages", 0) > 0 and item.get("completed_at")) | |
| # Dacă se pierd mai mult de 10 issues complete, STOP! | |
| if old_completed_count - new_completed_count > 10: | |
| print(f"\n{'='*70}") | |
| print(f"🚨 ALERTĂ CRITICĂ: PROTECȚIE ANTI-CORUPȚIE ACTIVATĂ!") | |
| print(f"{'='*70}") | |
| print(f"❌ Încercare de resetare masivă detectată:") | |
| print(f" Issues complete ÎNAINTE: {old_completed_count}") | |
| print(f" Issues complete DUPĂ: {new_completed_count}") | |
| print(f" Issues PIERDUTE: {old_completed_count - new_completed_count}") | |
| print(f"\n⚠️ SALVAREA A FOST BLOCATĂ pentru a preveni corupția datelor!") | |
| print(f" State.json NU a fost modificat.") | |
| print(f"{'='*70}\n") | |
| return # NU salva! | |
| # PASUL 3: Creează backup timestamped ÎNAINTE de salvare | |
| backup_dir = os.path.join(os.path.dirname(self.state_path), "State_Backups") | |
| os.makedirs(backup_dir, exist_ok=True) | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| backup_path = os.path.join(backup_dir, f"state_{timestamp}.json") | |
| if os.path.exists(self.state_path): | |
| shutil.copy2(self.state_path, backup_path) | |
| # Păstrează doar ultimele 100 de backup-uri | |
| backup_files = sorted(os.listdir(backup_dir)) | |
| if len(backup_files) > 100: | |
| for old_backup in backup_files[:-100]: | |
| os.remove(os.path.join(backup_dir, old_backup)) | |
| # PASUL 4: Loghează modificările (cu stack trace pentru debugging) | |
| import inspect | |
| caller_function = "Unknown" | |
| try: | |
| stack = inspect.stack() | |
| if len(stack) > 2: | |
| caller_function = f"{stack[2].function} (line {stack[2].lineno})" | |
| except: | |
| pass | |
| self._log_state_changes(old_state, self.state, caller_function) | |
| # PASUL 5: Salvează starea nouă | |
| with open(self.state_path, "w", encoding="utf-8") as f: | |
| json.dump(self.state, f, indent=2, ensure_ascii=False) | |
| except Exception as e: | |
| print(f"⚠ Nu am putut salva state-ul: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| # Încearcă să restabilească din backup zilnic | |
| backup_path = self.state_path + ".backup" | |
| if os.path.exists(backup_path): | |
| print(f"🔄 Încerc să restabilesc din backup...") | |
| try: | |
| shutil.copy2(backup_path, self.state_path) | |
| print(f"✅ State restabilit din backup") | |
| except: | |
| print(f"❌ Nu am putut restabili din backup") | |
| def _save_state(self): | |
| """WRAPPER: Folosește salvarea safe""" | |
| self._save_state_safe() | |
| def fix_existing_json(self): | |
| """Funcție temporară pentru a repara caracterele din JSON existent""" | |
| if os.path.exists(self.state_path): | |
| with open(self.state_path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| data = self._decode_unicode_escapes(data) | |
| with open(self.state_path, "w", encoding="utf-8") as f: | |
| json.dump(data, f, indent=2, ensure_ascii=False) | |
| print("✅ JSON reparat cu caractere românești") | |
| def remaining_quota(self): | |
| return max(0, DAILY_LIMIT - self.state.get("count", 0)) | |
| def _update_partial_issue_progress(self, issue_url, last_successful_segment_end, total_pages=None, title=None, subtitle=None): | |
| """FIXED: Previne dublurile - verifică și după title dacă URL-ul nu se potrivește""" | |
| normalized = issue_url.rstrip('/') | |
| updated = False | |
| # STEP 1: Caută după URL exact | |
| for i, item in enumerate(self.state.setdefault("downloaded_issues", [])): | |
| if item["url"] == normalized: | |
| # ACTUALIZEAZĂ issue-ul existent | |
| if last_successful_segment_end > item.get("last_successful_segment_end", 0): | |
| item["last_successful_segment_end"] = last_successful_segment_end | |
| if total_pages is not None and not item.get("total_pages"): | |
| item["total_pages"] = total_pages | |
| if title and not item.get("title"): | |
| item["title"] = title | |
| if subtitle and not item.get("subtitle"): | |
| item["subtitle"] = subtitle | |
| # Mută la început pentru prioritate | |
| updated_item = self.state["downloaded_issues"].pop(i) | |
| self.state["downloaded_issues"].insert(0, updated_item) | |
| updated = True | |
| print(f"🔄 ACTUALIZAT progres pentru: {normalized} → {last_successful_segment_end} pagini") | |
| break | |
| # STEP 2: Dacă nu găsești după URL, caută după title (prevenire dubluri) | |
| if not updated and title: | |
| for i, item in enumerate(self.state["downloaded_issues"]): | |
| if item.get("title") == title and not item["url"].startswith("http"): | |
| # GĂSIT dublu cu title ca URL - șterge-l! | |
| print(f"🗑️ ȘTERG DUBLU GREȘIT: {item['url']} (era title în loc de URL)") | |
| self.state["downloaded_issues"].pop(i) | |
| break | |
| # STEP 3: Doar dacă nu există deloc, creează nou | |
| if not updated: | |
| # VALIDEAZĂ că URL-ul e corect | |
| if not normalized.startswith("https://"): | |
| print(f"❌ URL INVALID: {normalized} - nu creez issue nou!") | |
| return | |
| new_issue = { | |
| "url": normalized, | |
| "title": title or "", | |
| "subtitle": subtitle or "", | |
| "pages": 0, | |
| "completed_at": "", | |
| "last_successful_segment_end": last_successful_segment_end, | |
| "total_pages": total_pages | |
| } | |
| self.state["downloaded_issues"].insert(0, new_issue) | |
| print(f"➕ ADĂUGAT issue nou în progres: {normalized}") | |
| self._save_state_safe() | |
| print(f"💾 Progres salvat SAFE: {normalized} - pagini {last_successful_segment_end}/{total_pages or '?'}") | |
| def mark_issue_done(self, issue_url, pages_count, title=None, subtitle=None, total_pages=None): | |
| """ULTRA SAFE: Verificări stricte înainte de a marca ca terminat + DETECTARE MAGHIARĂ""" | |
| normalized = issue_url.rstrip('/') | |
| now_iso = datetime.now().isoformat(timespec="seconds") | |
| print(f"🔒 VERIFICĂRI ULTRA SAFE pentru marcarea ca terminat: {normalized}") | |
| # VERIFICARE 0: Detectează posibila problemă cu maghiara | |
| if total_pages == 1 and pages_count == 1: | |
| print(f"🚨 ALERTĂ CRITICĂ: total_pages=1 și pages_count=1") | |
| print(f"🔍 Posibilă problemă de detectare pentru interfața maghiară!") | |
| print(f"🛡️ REFUZ să marchez ca terminat - probabil e o eroare!") | |
| # Încearcă să re-detecteze numărul corect de pagini | |
| print(f"🔄 Încerc re-detectarea numărului total de pagini...") | |
| try: | |
| if self.driver and self.current_issue_url == normalized: | |
| real_total = self.get_total_pages(max_attempts=3) | |
| if real_total > 1: | |
| print(f"✅ RE-DETECTAT: {real_total} pagini în loc de 1!") | |
| # Marchează ca parțial cu progresul real | |
| self._update_partial_issue_progress( | |
| normalized, pages_count, total_pages=real_total, title=title, subtitle=subtitle | |
| ) | |
| return | |
| except: | |
| pass | |
| print(f"🛡️ BLOCARE SAFETY: NU marchez issue-uri cu 1 pagină ca terminate!") | |
| return | |
| # VERIFICARE 2: pages_count trebuie să fie aproape de total_pages | |
| completion_percentage = (pages_count / total_pages) * 100 | |
| if completion_percentage < 95: # Trebuie să fie cel puțin 95% complet | |
| print(f"❌ BLOCARE SAFETY: Progres insuficient pentru {normalized}") | |
| print(f"📊 Progres: {pages_count}/{total_pages} ({completion_percentage:.1f}%)") | |
| print(f"🛡️ Trebuie cel puțin 95% pentru a marca ca terminat!") | |
| print(f"🔄 Marchează ca parțial în loc de terminat") | |
| # Marchează ca parțial, NU ca terminat | |
| self._update_partial_issue_progress( | |
| normalized, pages_count, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| return | |
| # VERIFICARE 3: Detectează batch size suspicious | |
| if pages_count < 100 and total_pages > 500: | |
| print(f"❌ BLOCARE SAFETY: Progres suspect de mic pentru {normalized}") | |
| print(f"📊 {pages_count} pagini par să fie doar primul batch din {total_pages}") | |
| print(f"🛡️ Probabil s-a oprit prematur, NU marchez ca terminat") | |
| # Marchează ca parțial | |
| self._update_partial_issue_progress( | |
| normalized, pages_count, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| return | |
| # VERIFICARE 4: Verifică dacă pages_count pare să fie doar primul segment | |
| if total_pages >= 1000 and pages_count < 100: | |
| print(f"❌ BLOCARE SAFETY: {pages_count} pagini din {total_pages} pare primul segment") | |
| print(f"🛡️ NU marchez issues mari ca terminate cu progres atât de mic") | |
| # Marchează ca parțial | |
| self._update_partial_issue_progress( | |
| normalized, pages_count, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| return | |
| # ===== TOATE VERIFICĂRILE AU TRECUT - SAFE SĂ MARCHEZ CA TERMINAT ===== | |
| print(f"✅ TOATE VERIFICĂRILE ULTRA SAFE trecute pentru {normalized}") | |
| print(f"📊 Progres: {pages_count}/{total_pages} ({completion_percentage:.1f}%)") | |
| print(f"🎯 Marchez ca TERMINAT") | |
| # Continuă cu logica originală de marcare ca terminat... | |
| existing = None | |
| existing_index = -1 | |
| # CĂUTARE ÎMBUNĂTĂȚITĂ: încearcă mai multe variante de URL | |
| search_variants = [ | |
| normalized, | |
| normalized + '/', | |
| normalized.replace('https://', 'http://'), | |
| normalized.replace('http://', 'https://') | |
| ] | |
| for i, item in enumerate(self.state.setdefault("downloaded_issues", [])): | |
| item_url = item.get("url", "").rstrip('/') | |
| if item_url in search_variants or normalized in [item_url, item_url + '/']: | |
| existing = item | |
| existing_index = i | |
| print(f"🔍 GĂSIT issue existent la index {i}: {item_url}") | |
| break | |
| # Creează record-ul de completare | |
| completion_data = { | |
| "pages": pages_count, | |
| "completed_at": now_iso, | |
| "last_successful_segment_end": pages_count, | |
| "total_pages": total_pages # SETEAZĂ ÎNTOTDEAUNA! | |
| } | |
| # Adaugă title/subtitle doar dacă nu există sau sunt goale | |
| if title: | |
| completion_data["title"] = title | |
| if subtitle: | |
| completion_data["subtitle"] = subtitle | |
| if existing: | |
| # ÎMBOGĂȚEȘTE issue-ul existent | |
| for key, value in completion_data.items(): | |
| if key in ["title", "subtitle"]: | |
| if not existing.get(key): | |
| existing[key] = value | |
| else: | |
| existing[key] = value | |
| # SCOATE din poziția curentă | |
| updated_issue = self.state["downloaded_issues"].pop(existing_index) | |
| print(f"✅ ACTUALIZAT și SCOS din poziția {existing_index}: {normalized}") | |
| else: | |
| # Creează issue nou complet | |
| updated_issue = { | |
| "url": normalized, | |
| "title": title or "", | |
| "subtitle": subtitle or "", | |
| **completion_data | |
| } | |
| print(f"➕ CREAT issue nou: {normalized}") | |
| # INSEREAZĂ ÎN POZIȚIA CRONOLOGICĂ CORECTĂ | |
| # Găsește primul issue cu completed_at mai vechi decât cel curent | |
| insert_position = 0 | |
| # Sari peste issue-urile parțiale (care sunt mereu primele) | |
| while (insert_position < len(self.state["downloaded_issues"]) and | |
| not self.state["downloaded_issues"][insert_position].get("completed_at")): | |
| insert_position += 1 | |
| # Găsește poziția corectă între issue-urile complete (sortate cronologic descendent) | |
| while insert_position < len(self.state["downloaded_issues"]): | |
| other_completed_at = self.state["downloaded_issues"][insert_position].get("completed_at", "") | |
| if other_completed_at and other_completed_at < now_iso: | |
| break | |
| insert_position += 1 | |
| # Inserează în poziția cronologică corectă | |
| self.state["downloaded_issues"].insert(insert_position, updated_issue) | |
| print(f"📅 INSERAT în poziția CRONOLOGICĂ {insert_position} (după issue-urile parțiale și în ordine de completed_at)") | |
| # Actualizează contoarele SAFE | |
| completed_count = len([i for i in self.state["downloaded_issues"] if i.get("completed_at")]) | |
| self.state["count"] = max(self.state.get("count", 0), completed_count) | |
| # Actualizează pages_downloaded SAFE | |
| current_pages = self.state.get("pages_downloaded", 0) | |
| self.state["pages_downloaded"] = current_pages + pages_count | |
| # Adaugă în recent_links (păstrează max 10) | |
| recent_entry = { | |
| "url": normalized, | |
| "title": (existing and existing.get("title")) or title or "", | |
| "subtitle": (existing and existing.get("subtitle")) or subtitle or "", | |
| "pages": pages_count, | |
| "timestamp": now_iso | |
| } | |
| recent_links = self.state.setdefault("recent_links", []) | |
| recent_links.insert(0, recent_entry) | |
| self.state["recent_links"] = recent_links[:10] | |
| # Resetează flag-ul de limită | |
| self.state["daily_limit_hit"] = False | |
| # Adaugă în skip URLs | |
| self.dynamic_skip_urls.add(normalized) | |
| # Adaugă în log zilnic | |
| self._log_completed_issue(normalized, title or "", subtitle or "", pages_count) | |
| self._save_state_safe() | |
| self._save_skip_urls() | |
| print(f"✅ Issue marcat ca terminat cu SORTARE CRONOLOGICĂ CORECTĂ: {normalized}") | |
| print(f"📊 Detalii: {pages_count} pagini, total_pages: {total_pages}") | |
| print(f"📊 Total complet: {self.state['count']}, Total pagini: {self.state['pages_downloaded']}") | |
| print(f"📅 Plasat în poziția cronologică {insert_position} din {len(self.state['downloaded_issues'])}") | |
| def mark_collection_complete(self, collection_url): | |
| """Marchează o colecție ca fiind complet procesată în skip_urls.json""" | |
| try: | |
| normalized_collection = collection_url.rstrip('/') | |
| # Adaugă în dynamic skip URLs | |
| self.dynamic_skip_urls.add(normalized_collection) | |
| # Salvează în skip_urls.json cu un marker special pentru colecții | |
| skip_data = {} | |
| if os.path.exists(self.skip_urls_path): | |
| with open(self.skip_urls_path, "r", encoding="utf-8") as f: | |
| skip_data = json.load(f) | |
| completed_collections = skip_data.get("completed_collections", []) | |
| if normalized_collection not in completed_collections: | |
| completed_collections.append(normalized_collection) | |
| skip_data["completed_collections"] = completed_collections | |
| skip_data["last_updated"] = datetime.now().isoformat() | |
| with open(self.skip_urls_path, "w", encoding="utf-8") as f: | |
| json.dump(skip_data, f, indent=2, ensure_ascii=False) | |
| print(f"✅ Colecția marcată ca completă: {normalized_collection}") | |
| except Exception as e: | |
| print(f"⚠ Eroare la marcarea colecției complete: {e}") | |
| def setup_chrome_driver(self): | |
| # Calea către scriptul batch care pornește Chrome în debug mode | |
| CHROME_DEBUG_SCRIPT = r"e:\Carte\BB\17 - Site Leadership\alte\Ionel Balauta\Aryeht\Task 1 - Traduce tot site-ul\Doar Google Web\Andreea\Meditatii\2023\++Arcanum Download + Chrome\Ruleaza cand sunt plecat 3\start_chrome_debug.bat" | |
| try: | |
| print("🔧 Inițializare WebDriver – încerc conectare la instanța Chrome existentă via remote debugging...") | |
| chrome_options = Options() | |
| chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222") | |
| prefs = { | |
| "download.default_directory": os.path.abspath(self.download_dir), | |
| "download.prompt_for_download": False, | |
| "download.directory_upgrade": True, | |
| "safebrowsing.enabled": True, | |
| } | |
| chrome_options.add_experimental_option("prefs", prefs) | |
| try: | |
| self.driver = webdriver.Chrome(options=chrome_options) | |
| self.wait = WebDriverWait(self.driver, self.timeout) | |
| self.attached_existing = True | |
| print("✅ Conectat la instanța Chrome existentă cu succes.") | |
| return True | |
| except WebDriverException as e: | |
| print(f"⚠ Conexiune la Chrome existent eșuat: {e}") | |
| print(f"🔄 Încerc să pornesc Chrome prin scriptul debug...") | |
| # Verifică dacă scriptul batch există | |
| if not os.path.exists(CHROME_DEBUG_SCRIPT): | |
| print(f"❌ EROARE: Scriptul Chrome debug nu există: {CHROME_DEBUG_SCRIPT}") | |
| print(f"⚠️ Chrome trebuie pornit MANUAL prin scriptul debug!") | |
| print(f"⏳ Așteaptă 30 secunde pentru pornire manuală...") | |
| time.sleep(30) | |
| # Reîncearcă conectarea | |
| try: | |
| self.driver = webdriver.Chrome(options=chrome_options) | |
| self.wait = WebDriverWait(self.driver, self.timeout) | |
| self.attached_existing = True | |
| print("✅ Conectat la Chrome după așteptare.") | |
| return True | |
| except: | |
| print("❌ Încă nu pot conecta la Chrome - opresc scriptul") | |
| return False | |
| # Pornește scriptul batch | |
| try: | |
| print(f"🚀 Pornesc Chrome prin: {CHROME_DEBUG_SCRIPT}") | |
| # Pornește scriptul în background (nu așteaptă finalizarea) | |
| subprocess.Popen([CHROME_DEBUG_SCRIPT], shell=True, | |
| creationflags=subprocess.CREATE_NO_WINDOW) | |
| print(f"⏳ Aștept 10 secunde pentru pornirea Chrome...") | |
| time.sleep(10) | |
| # Încearcă să se conecteze (cu retry) | |
| for attempt in range(1, 6): # 5 încercări | |
| print(f"🔄 Încercare conectare {attempt}/5...") | |
| try: | |
| self.driver = webdriver.Chrome(options=chrome_options) | |
| self.wait = WebDriverWait(self.driver, self.timeout) | |
| self.attached_existing = True | |
| print("✅ Conectat la Chrome după repornire cu succes!") | |
| return True | |
| except WebDriverException as retry_e: | |
| if attempt < 5: | |
| print(f"⚠️ Încercare {attempt} eșuată, reîncerc în 5 secunde...") | |
| time.sleep(5) | |
| else: | |
| print(f"❌ Nu am putut conecta după 5 încercări: {retry_e}") | |
| return False | |
| except Exception as script_error: | |
| print(f"❌ Eroare la pornirea scriptului Chrome: {script_error}") | |
| print(f"⚠️ Pornește MANUAL Chrome prin scriptul debug!") | |
| return False | |
| except WebDriverException as e: | |
| print(f"❌ Eroare la inițializarea WebDriver-ului: {e}") | |
| return False | |
| def navigate_to_page(self, url): | |
| try: | |
| # VERIFICĂ ÎNTÂI DACĂ BROWSER-UL MAI EXISTĂ | |
| try: | |
| _ = self.driver.current_url | |
| except: | |
| print("⚠ Browser închis, încerc reconectare...") | |
| if not self.setup_chrome_driver(): | |
| print("❌ Nu pot reconecta browser-ul") | |
| return False | |
| print(f"🌐 Navighez către: {url}") | |
| self.driver.get(url) | |
| self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'body'))) | |
| print("✅ Pagina încărcată.") | |
| # Așteaptă 2 secunde ca pagina să se stabilizeze | |
| time.sleep(5) | |
| # 🔧 VERIFICARE 1: MENTENANȚĂ (403 Forbidden) | |
| if self.detect_403_maintenance(): | |
| print(f"⚠️ Detectat 403 Forbidden - Arcanum în mentenanță") | |
| # Așteaptă finalul mentenanței (10 min x 3 încercări = 30 min max) | |
| if self.wait_for_maintenance(wait_minutes=10, max_retries=3): | |
| print(f"✅ Mentenanță finalizată - continuăm") | |
| # Site-ul e online, continuă | |
| else: | |
| print(f"❌ Mentenanță prea lungă - opresc scriptul") | |
| return False | |
| # 🚨 VERIFICARE 2: CAPTCHA | |
| if self.detect_captcha(): | |
| print(f"\n{'='*60}") | |
| print(f"🚨🚨🚨 CAPTCHA DETECTAT - OPRIRE COMPLETĂ! 🚨🚨🚨") | |
| print(f"{'='*60}") | |
| print(f"❌ Sistemul Arcanum necesită verificare umană (CAPTCHA)") | |
| print(f"❌ Scriptul NU poate rezolva CAPTCHA automat") | |
| print(f"💾 Salvez progresul curent și opresc scriptul...") | |
| self.state["captcha_detected"] = True | |
| self.state["captcha_url"] = self.driver.current_url | |
| self._save_state() | |
| print(f"\n🛑 SCRIPTUL A FOST OPRIT DIN CAUZA CAPTCHA") | |
| print(f"📋 URL CAPTCHA: {self.driver.current_url}") | |
| print(f"📋 Progresul a fost salvat în state.json") | |
| print(f"⚠️ ACȚIUNE NECESARĂ: Rezolvă CAPTCHA manual în browser") | |
| print(f"{'='*60}\n") | |
| raise SystemExit("🚨 OPRIRE CAPTCHA - Verificare umană necesară!") | |
| return True | |
| except SystemExit: | |
| # Re-ridică SystemExit pentru a opri scriptul complet | |
| raise | |
| except Exception as e: | |
| print(f"❌ Eroare la navigare sau încărcare: {e}") | |
| # ÎNCEARCĂ O RECONECTARE CA ULTIM RESORT | |
| try: | |
| print("🔄 Încerc reconectare de urgență...") | |
| if self.setup_chrome_driver(): | |
| self.driver.get(url) | |
| self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'body'))) | |
| print("✅ Reconectat și navigat cu succes!") | |
| time.sleep(2) | |
| # 🔧 VERIFICARE 403 DUPĂ RECONECTARE | |
| if self.detect_403_maintenance(): | |
| print(f"⚠️ 403 după reconectare") | |
| if self.wait_for_maintenance(wait_minutes=10, max_retries=3): | |
| print(f"✅ Mentenanță finalizată după reconectare") | |
| else: | |
| print(f"❌ Mentenanță prea lungă după reconectare") | |
| return False | |
| # 🚨 VERIFICARE CAPTCHA DUPĂ RECONECTARE | |
| if self.detect_captcha(): | |
| print(f"\n🚨 CAPTCHA DETECTAT DUPĂ RECONECTARE - OPRIRE!") | |
| self.state["captcha_detected"] = True | |
| self.state["captcha_url"] = self.driver.current_url | |
| self._save_state() | |
| raise SystemExit("🚨 OPRIRE CAPTCHA - Verificare umană necesară!") | |
| return True | |
| except SystemExit: | |
| raise | |
| except: | |
| pass | |
| return False | |
| def get_issue_metadata(self): | |
| title = "" | |
| subtitle = "" | |
| try: | |
| breadcrumb = self.driver.find_element(By.CSS_SELECTOR, "li.breadcrumb-item.active") | |
| try: | |
| sub_elem = breadcrumb.find_element(By.CSS_SELECTOR, "#pdfview-pdfcontents span") | |
| subtitle = sub_elem.text.strip() | |
| except Exception: | |
| subtitle = "" | |
| raw = breadcrumb.text.strip() | |
| if subtitle and subtitle in raw: | |
| title = raw.replace(subtitle, "").strip() | |
| else: | |
| title = raw | |
| except Exception: | |
| pass | |
| return title, subtitle | |
| def get_total_pages(self, max_attempts=5, delay_between=1.0): | |
| """FIXED: Detectează corect numărul total de pagini INCLUSIV pentru limba maghiară""" | |
| for attempt in range(1, max_attempts + 1): | |
| try: | |
| # Metoda 1: Caută pattern-uri specifice pentru maghiară ȘI alte limbi | |
| page_patterns = [ | |
| r'(\d+)\s*/\s*(\d+)', # "1 / 146" (română/engleză) | |
| r'/\s*(\d+)', # "/ 146" (maghiară - PRINCIPAL) | |
| r'of\s+(\d+)', # "of 146" (engleză) | |
| r'din\s+(\d+)', # "din 146" (română) | |
| r'(\d+)\s*oldal', # "146 oldal" (maghiară) | |
| r'összesen\s+(\d+)', # "összesen 146" (maghiară) | |
| ] | |
| # PRIORITATE: Caută mai întâi în clasa CSS specifică maghiară | |
| try: | |
| # Pattern specific pentru interfața maghiară din screenshot | |
| adornment_divs = self.driver.find_elements(By.CSS_SELECTOR, | |
| 'div.MuiInputAdornment-root.MuiInputAdornment-positionEnd') | |
| for div in adornment_divs: | |
| text = div.text.strip() | |
| print(f"🔍 Verific div adornment: '{text}'") | |
| # Caută pattern-ul "/ 146" | |
| match = re.search(r'/\s*(\d+)', text) | |
| if match: | |
| total = int(match.group(1)) | |
| print(f"✅ TOTAL PAGINI detectat din adornment maghiar: {total}") | |
| return total | |
| except Exception as e: | |
| print(f"⚠ Eroare în detectare maghiară: {e}") | |
| # Metoda 2: Caută în toate elementele cu text (backup) | |
| all_texts = self.driver.find_elements(By.XPATH, | |
| "//*[contains(text(), '/') or contains(text(), 'of') or contains(text(), 'din') or contains(text(), 'oldal')]") | |
| for el in all_texts: | |
| text = el.text.strip() | |
| print(f"🔍 Verific text element: '{text}'") | |
| for pattern in page_patterns: | |
| matches = re.findall(pattern, text) | |
| if matches: | |
| if pattern == page_patterns[0]: # "număr / total" | |
| current, total = matches[0] | |
| total = int(total) | |
| print(f"✅ TOTAL PAGINI detectat din '{text}': {total} (curent: {current})") | |
| return total | |
| else: # "/ total", "of total", etc. | |
| total = int(matches[0]) | |
| print(f"✅ TOTAL PAGINI detectat din '{text}': {total}") | |
| return total | |
| # Metoda 3: JavaScript mai robust pentru maghiară | |
| js_result = self.driver.execute_script(r""" | |
| const patterns = [ | |
| /\/\s*(\d+)/g, // / 146 (PRIORITATE pentru maghiară) | |
| /(\d+)\s*\/\s*(\d+)/g, // 1 / 146 | |
| /of\s+(\d+)/g, // of 146 | |
| /din\s+(\d+)/g, // din 146 | |
| /(\d+)\s*oldal/g, // 146 oldal | |
| /összesen\s+(\d+)/g // összesen 146 | |
| ]; | |
| // Caută în toate nodurile text | |
| const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); | |
| const results = []; | |
| while(walker.nextNode()) { | |
| const text = walker.currentNode.nodeValue; | |
| if (!text || text.trim().length < 2) continue; | |
| for (let pattern of patterns) { | |
| const matches = [...text.matchAll(pattern)]; | |
| if (matches.length > 0) { | |
| const match = matches[0]; | |
| let total, current = 0; | |
| if (match.length === 3) { // "număr / total" | |
| current = parseInt(match[1]); | |
| total = parseInt(match[2]); | |
| } else { // "/ total" | |
| total = parseInt(match[1]); | |
| } | |
| if (total && total > 0) { | |
| results.push({ | |
| text: text.trim(), | |
| total: total, | |
| current: current, | |
| pattern: pattern.source | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| // Sortează după total (cel mai mare primul) și returnează primul | |
| results.sort((a, b) => b.total - a.total); | |
| return results.length > 0 ? results[0] : null; | |
| """) | |
| if js_result: | |
| total = js_result['total'] | |
| current = js_result.get('current', 0) | |
| text = js_result['text'] | |
| pattern = js_result['pattern'] | |
| print(f"✅ TOTAL PAGINI detectat prin JS: {total} din '{text}' (pattern: {pattern})") | |
| return total | |
| print(f"⚠ ({attempt}) Nu am găsit încă numărul total de pagini, reîncerc în {delay_between}s...") | |
| time.sleep(delay_between) | |
| except Exception as e: | |
| print(f"⚠ ({attempt}) Eroare în get_total_pages: {e}") | |
| time.sleep(delay_between) | |
| print("❌ Nu s-a reușit extragerea numărului total de pagini după multiple încercări.") | |
| return 0 | |
| def debug_page_detection(self): | |
| """Funcție de debugging pentru a vedea ce detectează în interfața maghiară""" | |
| try: | |
| print("🔍 DEBUG: Analizez interfața pentru detectarea paginilor...") | |
| # 1. Verifică adornment-urile | |
| adornments = self.driver.find_elements(By.CSS_SELECTOR, | |
| 'div.MuiInputAdornment-root') | |
| print(f"📊 Găsite {len(adornments)} adornment-uri:") | |
| for i, div in enumerate(adornments): | |
| text = div.text.strip() | |
| html = div.get_attribute('outerHTML')[:100] | |
| print(f" {i+1}. Text: '{text}' | HTML: {html}...") | |
| # 2. Caută toate elementele cu "/" | |
| slash_elements = self.driver.find_elements(By.XPATH, "//*[contains(text(), '/')]") | |
| print(f"📊 Găsite {len(slash_elements)} elemente cu '/':") | |
| for i, el in enumerate(slash_elements[:5]): # Primele 5 | |
| text = el.text.strip() | |
| tag = el.tag_name | |
| print(f" {i+1}. <{tag}>: '{text}'") | |
| # 3. JavaScript debug | |
| js_result = self.driver.execute_script(""" | |
| const allText = document.body.innerText; | |
| const lines = allText.split('\\n'); | |
| const relevantLines = lines.filter(line => | |
| line.includes('/') || | |
| line.includes('oldal') || | |
| line.includes('összesen') | |
| ); | |
| return relevantLines.slice(0, 10); | |
| """) | |
| print(f"📊 Linii relevante din JS:") | |
| for i, line in enumerate(js_result): | |
| print(f" {i+1}. '{line.strip()}'") | |
| except Exception as e: | |
| print(f"❌ Eroare în debug: {e}") | |
| def open_save_popup(self): | |
| try: | |
| try: | |
| self.wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, 'div.MuiDialog-container'))) | |
| except Exception: | |
| self.driver.switch_to.active_element.send_keys(Keys.ESCAPE) | |
| time.sleep(0.5) | |
| svg = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'svg[data-testid="SaveAltIcon"]'))) | |
| button = svg | |
| if svg.tag_name.lower() == "svg": | |
| try: | |
| button = svg.find_element(By.XPATH, "./ancestor::button") | |
| except Exception: | |
| pass | |
| for attempt in range(1, 4): | |
| try: | |
| button.click() | |
| return True | |
| except ElementClickInterceptedException: | |
| print(f"⚠ click interceptat (încercarea {attempt}), trimit ESC și reiau...") | |
| self.driver.switch_to.active_element.send_keys(Keys.ESCAPE) | |
| time.sleep(1) | |
| continue | |
| print("❌ Nu am reușit să dau click pe butonul de deschidere a popup-ului după retry-uri.") | |
| return False | |
| except Exception as e: | |
| print(f"❌ Nu am reușit să deschid popup-ul de salvare: {e}") | |
| return False | |
| def detect_save_button_multilingual(self): | |
| """ | |
| Detectează butonul de salvare în orice limbă suportată de Arcanum | |
| """ | |
| # Lista cu toate variantele de text pentru butonul de salvare | |
| save_button_texts = [ | |
| "Salvați", # Română | |
| "Save", # Engleză | |
| "Mentés", # Maghiară | |
| "Uložiť", # Slovacă/Cehă | |
| "Speichern", # Germană | |
| "Salvar", # Spaniolă (dacă e cazul) | |
| "Sauvegarder" # Franceză (dacă e cazul) | |
| ] | |
| for text in save_button_texts: | |
| try: | |
| save_btn = self.driver.find_element(By.XPATH, | |
| f'//button[.//text()[contains(normalize-space(.), "{text}")]]') | |
| if save_btn and save_btn.is_enabled(): | |
| print(f"✅ Buton de salvare găsit cu textul: '{text}'") | |
| return save_btn | |
| except: | |
| continue | |
| # Dacă nu găsește cu textul, încearcă după clasele CSS (backup method) | |
| try: | |
| buttons = self.driver.find_elements(By.CSS_SELECTOR, | |
| 'button[class*="MuiButton"][class*="Primary"]') | |
| for btn in buttons: | |
| text = btn.text.strip().lower() | |
| # Verifică dacă conține cuvinte cheie în orice limbă | |
| if any(keyword in text for keyword in ['salv', 'save', 'ment', 'ulož', 'speich']): | |
| print(f"✅ Buton de salvare găsit prin CSS cu textul: '{btn.text}'") | |
| return btn | |
| except: | |
| pass | |
| return None | |
| def fill_and_save_range(self, start, end): | |
| try: | |
| first_input = self.wait.until(EC.presence_of_element_located((By.ID, "first page"))) | |
| print("⏳ Aștept 5s înainte de a completa primul input...") | |
| time.sleep(1) # Incepand de la | |
| first_input.send_keys(Keys.CONTROL + "a") | |
| first_input.send_keys(str(start)) | |
| print(f"✏️ Am introdus primul număr: {start}") | |
| print("⏳ Aștept 5s înainte de a completa al doilea input...") | |
| time.sleep(1) # Pana la | |
| last_input = self.wait.until(EC.presence_of_element_located((By.ID, "last page"))) | |
| last_input.send_keys(Keys.CONTROL + "a") | |
| last_input.send_keys(str(end)) | |
| print(f"✏️ Am introdus al doilea număr: {end}") | |
| print("⏳ Aștept 5s înainte de a apăsa butonul de salvare...") | |
| time.sleep(1) # Salvati | |
| # Utilizează funcția multilingvă pentru a găsi butonul | |
| save_btn = self.detect_save_button_multilingual() | |
| if save_btn: | |
| save_btn.click() | |
| print(f"✅ Segmentul {start}-{end} salvat.") | |
| return True | |
| else: | |
| print(f"❌ Nu am găsit butonul de salvare în nicio limbă pentru segmentul {start}-{end}") | |
| return False | |
| except Exception as e: | |
| print(f"❌ Eroare la completarea/salvarea intervalului {start}-{end}: {e}") | |
| return False | |
| def check_daily_limit_in_all_windows(self, set_flag=True): | |
| # return False # Add this line at the top to disable detection | |
| """Verifică mesajul de limită zilnică în toate ferestrele deschise""" | |
| current_window = self.driver.current_window_handle | |
| limit_reached = False | |
| try: | |
| all_handles = self.driver.window_handles | |
| for handle in all_handles: | |
| try: | |
| self.driver.switch_to.window(handle) | |
| body_text = self.driver.find_element(By.TAG_NAME, "body").text | |
| if ("Daily download limit reached" in body_text or | |
| "Terms and conditions" in body_text): | |
| print(f"⚠ Limita zilnică detectată în fereastra: {handle}") | |
| limit_reached = True | |
| if handle != current_window and len(all_handles) > 1: | |
| print(f"🗙 Închid fereastra cu limita zilnică: {handle}") | |
| self.driver.close() | |
| break | |
| except Exception as e: | |
| continue | |
| try: | |
| if current_window in self.driver.window_handles: | |
| self.driver.switch_to.window(current_window) | |
| elif self.driver.window_handles: | |
| self.driver.switch_to.window(self.driver.window_handles[0]) | |
| except Exception: | |
| pass | |
| except Exception as e: | |
| print(f"⚠ Eroare la verificarea ferestrelor: {e}") | |
| if limit_reached and set_flag: | |
| self.state["daily_limit_hit"] = True | |
| self._save_state() | |
| return limit_reached | |
| def detect_captcha(self): | |
| """ | |
| 🚨 FUNCȚIE CRITICĂ: Detectează CAPTCHA Arcanum și oprește scriptul complet | |
| Detectează 3 tipuri de CAPTCHA: | |
| 1. Textul "Let's confirm you are human" + butonul "amzn-captcha-verify-button" | |
| 2. Pagina "Human Verification" cu "JavaScript is disabled" | |
| 3. Butonul "amzn-captcha-verify-button" (backup) | |
| Returns: True dacă CAPTCHA detectat, False altfel | |
| """ | |
| try: | |
| current_url = self.driver.current_url | |
| # Detectare 1: Verifică conținutul paginii (HTML și text) | |
| try: | |
| page_source = self.driver.page_source | |
| body_text = self.driver.find_element(By.TAG_NAME, "body").text | |
| # CAPTCHA Tip 1: "Let's confirm you are human" | |
| if (('<h1 style="font-weight: normal; color: rgb(221, 107, 16);">Let\'s confirm you are human</h1>' in page_source or | |
| 'Let\'s confirm you are human' in body_text) and | |
| 'Complete the security check before continuing' in body_text): | |
| print(f"\n{'='*60}") | |
| print(f"🚨 CAPTCHA DETECTAT (Tip 1) - VERIFICARE UMANĂ!") | |
| print(f"{'='*60}") | |
| print(f"URL: {current_url}") | |
| print(f"🛑 CAPTCHA detectat: 'Let's confirm you are human'") | |
| return True | |
| # CAPTCHA Tip 2: "Human Verification" cu JavaScript disabled message | |
| if ('<title>Human Verification</title>' in page_source and | |
| 'JavaScript is disabled' in page_source and | |
| 'you need to verify that you\'re not a robot by solving a CAPTCHA puzzle' in page_source): | |
| print(f"\n{'='*60}") | |
| print(f"🚨 CAPTCHA DETECTAT (Tip 2) - HUMAN VERIFICATION!") | |
| print(f"{'='*60}") | |
| print(f"URL: {current_url}") | |
| print(f"🛑 CAPTCHA detectat: 'Human Verification' page") | |
| return True | |
| except Exception: | |
| pass | |
| # Detectare 3: Verifică butonul CAPTCHA | |
| try: | |
| captcha_button = self.driver.find_element(By.ID, "amzn-captcha-verify-button") | |
| if captcha_button: | |
| print(f"\n{'='*60}") | |
| print(f"🚨 CAPTCHA DETECTAT (Tip 3) - BUTON CAPTCHA!") | |
| print(f"{'='*60}") | |
| print(f"URL: {current_url}") | |
| print(f"🛑 CAPTCHA detectat - buton de verificare găsit!") | |
| return True | |
| except Exception: | |
| pass | |
| return False | |
| except Exception as e: | |
| print(f"⚠ Eroare la detectarea CAPTCHA: {e}") | |
| return False | |
| def detect_403_maintenance(self): | |
| """ | |
| 🔧 Detectează mentenanța Arcanum (403 Forbidden) | |
| Când apare 403, înseamnă că Arcanum face mentenanță ~10 minute | |
| Scriptul va aștepta automat și va reîncerca | |
| IMPORTANT: Detectarea trebuie să fie STRICTĂ pentru a evita false pozitive! | |
| O pagină cu "403" în conținut normal (ex: anul 1403) NU e mentenanță. | |
| Returns: True dacă 403 detectat, False altfel | |
| """ | |
| try: | |
| # Verifică titlul paginii - dacă titlul e explicit "403 Forbidden" | |
| try: | |
| page_title = self.driver.title.strip().lower() | |
| # Titlul trebuie să fie EXACT sau să înceapă cu 403 | |
| if page_title in ["403 forbidden", "403", "forbidden"] or page_title.startswith("403"): | |
| print(f"⚠️ DETECTAT: 403 Forbidden în titlu: '{self.driver.title}'") | |
| return True | |
| except Exception: | |
| pass | |
| # Verifică conținutul paginii - dar DOAR dacă e o pagină de eroare scurtă | |
| try: | |
| body_text = self.driver.find_element(By.TAG_NAME, "body").text.strip() | |
| # Pagina de eroare 403 e FOARTE scurtă (sub 500 caractere de obicei) | |
| # și conține EXACT "403 Forbidden" ca text principal | |
| if len(body_text) < 500: | |
| # Verifică dacă e o pagină de eroare nginx/apache | |
| body_lower = body_text.lower() | |
| if ("403 forbidden" in body_lower and | |
| ("nginx" in body_lower or "apache" in body_lower or len(body_text) < 100)): | |
| print(f"⚠️ DETECTAT: Pagină de eroare 403 (server)") | |
| return True | |
| # Verifică dacă body-ul e aproape DOAR "403 Forbidden" | |
| if body_text.strip() in ["403 Forbidden", "403", "Forbidden", "Access Denied"]: | |
| print(f"⚠️ DETECTAT: Pagină de eroare 403 simplă") | |
| return True | |
| except Exception: | |
| pass | |
| # Verifică h1 cu 403 - dar DOAR dacă e exact "403 Forbidden" sau similar | |
| try: | |
| h1_elements = self.driver.find_elements(By.TAG_NAME, "h1") | |
| for h1 in h1_elements: | |
| h1_text = h1.text.strip().lower() | |
| # Verifică EXACT match pentru erori 403 | |
| if h1_text in ["403 forbidden", "403", "forbidden", "access denied"]: | |
| print(f"⚠️ DETECTAT: 403 Forbidden în header H1") | |
| return True | |
| except Exception: | |
| pass | |
| return False | |
| except Exception as e: | |
| print(f"⚠ Eroare la detectarea 403: {e}") | |
| return False | |
| def wait_for_maintenance(self, wait_minutes=10, max_retries=3): | |
| """ | |
| 🔧 Așteaptă finalul mentenanței Arcanum și reîncearcă | |
| Args: | |
| wait_minutes: Minute de așteptat între încercări (default: 10) | |
| max_retries: Număr maxim de reîncercări (default: 3) | |
| Returns: True dacă site-ul revine online, False dacă depășește max_retries | |
| """ | |
| print(f"\n{'='*60}") | |
| print(f"🔧 MENTENANȚĂ ARCANUM DETECTATĂ (403 Forbidden)") | |
| print(f"{'='*60}") | |
| print(f"⚠️ Site-ul Arcanum este în mentenanță") | |
| print(f"⏳ Aștept {wait_minutes} minute și reîncerc automat") | |
| print(f"📊 Reîncercări rămase: {max_retries}") | |
| for retry in range(1, max_retries + 1): | |
| print(f"\n🔄 ÎNCERCARE {retry}/{max_retries}") | |
| print(f"⏳ Aștept {wait_minutes} minute pentru finalizarea mentenanței...") | |
| # Așteptare cu afișare progress | |
| wait_seconds = wait_minutes * 60 | |
| interval = 60 # Afișează progress la fiecare minut | |
| for elapsed in range(0, wait_seconds, interval): | |
| remaining = wait_seconds - elapsed | |
| print(f" ⏱️ Aștept: {remaining // 60} minute rămase...") | |
| time.sleep(min(interval, remaining)) | |
| print(f"✅ Așteptare finalizată - încerc refresh...") | |
| # Dă refresh la pagină | |
| try: | |
| print(f"🔄 Dau refresh la pagină...") | |
| self.driver.refresh() | |
| time.sleep(5) # Așteaptă încărcarea | |
| # Verifică dacă 403 a dispărut | |
| if not self.detect_403_maintenance(): | |
| print(f"\n{'='*60}") | |
| print(f"✅ MENTENANȚA S-A TERMINAT!") | |
| print(f"{'='*60}") | |
| print(f"✅ Site-ul Arcanum este din nou online") | |
| print(f"🔄 Reiau descărcarea de unde am rămas...") | |
| return True | |
| else: | |
| print(f"❌ Încă 403 - mentenanța continuă...") | |
| if retry < max_retries: | |
| print(f"🔄 Mai încerc încă {max_retries - retry} ori...") | |
| except Exception as e: | |
| print(f"⚠ Eroare la refresh: {e}") | |
| # Depășit numărul maxim de reîncercări | |
| print(f"\n{'='*60}") | |
| print(f"❌ MENTENANȚA DEPĂȘEȘTE TIMPUL AȘTEPTAT") | |
| print(f"{'='*60}") | |
| print(f"❌ Am așteptat {wait_minutes * max_retries} minute total") | |
| print(f"❌ Site-ul încă returnează 403 Forbidden") | |
| print(f"💾 Salvez progresul și opresc scriptul") | |
| print(f"🔄 Repornește scriptul mai târziu când mentenanța se termină") | |
| return False | |
| def check_for_daily_limit_popup(self): | |
| """ | |
| FIXED: Verifică dacă s-a deschis o filă nouă cu mesajul de limită zilnică după descărcare. | |
| EXCLUDERE EXPLICITĂ pentru about:blank și alte file normale de browser | |
| """ | |
| try: | |
| current_handles = set(self.driver.window_handles) | |
| print(f"🔍 Verific {len(current_handles)} file deschise pentru limita zilnică...") | |
| # Verifică toate filele deschise pentru mesajul de limită | |
| for handle in current_handles: | |
| try: | |
| self.driver.switch_to.window(handle) | |
| # Obține URL-ul pentru debugging | |
| current_url = self.driver.current_url | |
| # SKIP EXPLICIT pentru about:blank și alte file normale de browser | |
| if (current_url == "about:blank" or | |
| current_url.startswith("chrome://") or | |
| current_url.startswith("chrome-extension://") or | |
| current_url.startswith("data:") or | |
| not current_url or current_url.strip() == ""): | |
| print(f"✅ Skip filă normală de browser: {current_url}") | |
| continue | |
| # Obține textul complet al paginii | |
| body_text = self.driver.find_element(By.TAG_NAME, "body").text.strip() | |
| # Obține sursa HTML pentru verificarea structurii | |
| try: | |
| page_source = self.driver.page_source | |
| except: | |
| page_source = "" | |
| # DETECTOARE MULTIPLE pentru limita zilnică | |
| limit_detected = False | |
| detection_reason = "" | |
| # 1. NOUA PAGINĂ: "Vezi Termeni de utilizare" | |
| if ("Vezi" in body_text and | |
| ("Termeni de utilizare" in body_text or "conditii-de-utilizare" in current_url)): | |
| limit_detected = True | |
| detection_reason = "NOUĂ PAGINĂ - Vezi Termeni de utilizare" | |
| # 2. VECHEA PAGINĂ: "Daily download limit reached" | |
| elif "Daily download limit reached" in body_text: | |
| limit_detected = True | |
| detection_reason = "VECHE PAGINĂ - Daily download limit reached" | |
| # 3. DETECTARE PRIN URL: dacă URL-ul conține "conditii-de-utilizare" | |
| elif "conditii-de-utilizare" in current_url: | |
| limit_detected = True | |
| detection_reason = "URL DETECTARE - conditii-de-utilizare" | |
| # 4. DETECTARE PRIN LINK: caută linkul specific | |
| elif "www.arcanum.com/ro/adt/conditii-de-utilizare" in body_text: | |
| limit_detected = True | |
| detection_reason = "LINK DETECTARE - arcanum.com conditii" | |
| # 4. DETECTARE PRIN LINK: caută linkul specific | |
| elif "www.arcanum.com/en/adt/terms-and-conditions" in body_text: | |
| limit_detected = True | |
| detection_reason = "LINK DETECTARE - arcanum.com conditii" | |
| # 4. DETECTARE PRIN LINK: caută linkul specific | |
| elif "www.arcanum.com/hu/adt/felhasznalasi-feltetelek" in body_text: | |
| limit_detected = True | |
| detection_reason = "LINK DETECTARE - arcanum.com conditii" | |
| # 5. **NOUĂ DETECTARE**: Verifică structura HTML normală | |
| elif page_source and not self._has_normal_html_structure(page_source): | |
| # Verifică dacă e o pagină anormală (fără structura HTML standard) | |
| # și dacă conținutul e suspect de mic sau conține cuvinte cheie | |
| # DOAR dacă nu e about:blank (deja verificat mai sus) | |
| if (len(body_text.strip()) < 500 and | |
| (any(keyword in body_text.lower() for keyword in | |
| ['limit', 'vezi', 'termeni', 'utilizare', 'download', 'reached', 'Download-Limit']) or | |
| len(body_text.strip()) < 100)): | |
| limit_detected = True | |
| detection_reason = "STRUCTURĂ HTML ANORMALĂ - probabil pagină de limită" | |
| # 6. DETECTARE GENERALĂ: pagină suspicioasă cu puțin conținut și "Vezi" | |
| elif (len(body_text.strip()) < 200 and | |
| "Vezi" in body_text and | |
| len(body_text.split()) < 20): | |
| limit_detected = True | |
| detection_reason = "DETECTARE GENERALĂ - pagină suspicioasă cu 'Vezi'" | |
| # DEBUGGING: Afișează conținutul suspicios | |
| if (self._is_suspicious_page(body_text, current_url, page_source)): | |
| html_structure_ok = self._has_normal_html_structure(page_source) | |
| print(f"🔍 FILĂ SUSPICIOASĂ {handle}:") | |
| print(f" 📄 URL: {current_url}") | |
| print(f" 📝 Conținut ({len(body_text)} chars): '{body_text[:200]}{'...' if len(body_text) > 200 else ''}'") | |
| print(f" 🏗️ Structură HTML normală: {html_structure_ok}") | |
| print(f" 🎯 Detectat limit: {limit_detected} ({detection_reason})") | |
| # ACȚIUNE: Dacă limita a fost detectată | |
| if limit_detected: | |
| print(f"🛑 LIMITĂ ZILNICĂ DETECTATĂ în filă: {handle}") | |
| print(f"🔍 MOTIV: {detection_reason}") | |
| print(f"📄 URL complet: {current_url}") | |
| print(f"📝 Conținut complet filă:") | |
| print(f" '{body_text}'") | |
| print(f"🏗️ Structură HTML: {self._has_normal_html_structure(page_source)}") | |
| # Închide fila cu limita (dar doar dacă nu e singura filă) | |
| if len(current_handles) > 1: | |
| print(f"🗙 Închid fila cu limita: {handle}") | |
| self.driver.close() | |
| # Revine la prima filă disponibilă | |
| if self.driver.window_handles: | |
| self.driver.switch_to.window(self.driver.window_handles[0]) | |
| print(f"↩️ Revin la fila principală") | |
| else: | |
| print(f"⚠ Nu închid fila - este singura rămasă") | |
| # Setează flag-ul și oprește procesarea | |
| self.state["daily_limit_hit"] = True | |
| self._save_state() | |
| print(f"🛑 Flag daily_limit_hit setat în state.json") | |
| return True | |
| except Exception as e: | |
| print(f"⚠ Eroare la verificarea filei {handle}: {e}") | |
| continue | |
| print(f"✅ Nu am detectat limita zilnică în {len(current_handles)} file") | |
| return False | |
| except Exception as e: | |
| print(f"❌ Eroare fatală în verificarea popup-ului de limită: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return False | |
| def detect_login_required(self): | |
| """Detectează dacă este necesară autentificarea""" | |
| try: | |
| body_text = self.driver.find_element(By.TAG_NAME, "body").text | |
| current_url = self.driver.current_url | |
| # Verifică dacă există mesajul de login required | |
| if "Accesarea documentelor necesită abonament" in body_text: | |
| print("🔐 Detectat: Este necesară autentificarea (mesaj)") | |
| return True | |
| # Verifică și prin URL | |
| if "/accounts/login/" in current_url: | |
| print("🔐 Detectat: Pagină de login (URL)") | |
| return True | |
| return False | |
| except Exception as e: | |
| print(f"⚠ Eroare la detectarea login page: {e}") | |
| return False | |
| def handle_windows_auth_popup(self): | |
| """ | |
| 🔐 Gestionează pop-up-ul de autentificare Windows/Chrome | |
| Uneori Chrome cere autentificare Windows înainte de login. | |
| Avem 2 opțiuni: | |
| 1. Închide pop-up-ul (ESC) și continuă cu login normal | |
| 2. Completează parola Windows și continuă | |
| Returns: True dacă a reușit, False altfel | |
| """ | |
| try: | |
| print("\n🔍 Verific dacă apare pop-up Windows de autentificare...") | |
| time.sleep(5) # Așteaptă 2 secunde să apară eventualul pop-up | |
| # Încercăm să importăm pyautogui pentru interacțiunea cu Windows | |
| try: | |
| import pyautogui | |
| # OPȚIUNEA 1: Încearcă să închidă pop-up-ul cu ESC | |
| print("🔄 OPȚIUNEA 1: Încerc să închid pop-up-ul Windows cu ESC...") | |
| pyautogui.press('esc') | |
| time.sleep(1) | |
| # Verifică dacă pagina s-a încărcat normal | |
| try: | |
| self.driver.find_element(By.TAG_NAME, "body") | |
| print("✅ Pop-up închis cu succes - pagina accesibilă!") | |
| return True | |
| except: | |
| pass | |
| # OPȚIUNEA 2: Dacă ESC nu a funcționat, completează parola Windows | |
| print("🔄 OPȚIUNEA 2: Completez parola Windows...") | |
| print("⚠️ Dacă apare pop-up de autentificare Windows, completez automat...") | |
| # Așteaptă puțin | |
| time.sleep(1) | |
| # Scrie parola Windows | |
| windows_password = "_bebe@123##" | |
| pyautogui.write(windows_password, interval=0.1) | |
| time.sleep(0.5) | |
| # Apasă Enter pentru a confirma | |
| pyautogui.press('enter') | |
| time.sleep(2) | |
| print("✅ Parolă Windows introdusă și confirmată") | |
| return True | |
| except ImportError: | |
| print("⚠️ pyautogui nu este instalat - nu pot gestiona pop-up Windows automat") | |
| print("📋 Instalează cu: pip install pyautogui") | |
| print("⚠️ Dacă apare pop-up, închide-l manual sau introdu parola: _bebe@123##") | |
| time.sleep(5) # Dă timp utilizatorului să intervină manual | |
| return True | |
| except Exception as e: | |
| print(f"⚠️ Eroare la gestionarea pop-up Windows: {e}") | |
| print("🔄 Continui oricum cu login-ul normal...") | |
| return True | |
| def perform_auto_login(self): | |
| """Efectuează login automat când este detectată pagina de autentificare""" | |
| try: | |
| print("\n" + "="*60) | |
| print("🔐 ÎNCEPUT LOGIN AUTOMAT") | |
| print("="*60) | |
| # PASUL 0: Gestionează pop-up-ul Windows de autentificare (dacă apare) | |
| print("\n🔐 PASUL 0: Verific pop-up Windows de autentificare...") | |
| self.handle_windows_auth_popup() | |
| current_url = self.driver.current_url | |
| page_source = self.driver.page_source | |
| # PASUL 1: Dacă suntem pe pagina cu mesaj, găsește linkul "Conectare" | |
| if "Accesarea documentelor necesită abonament" in page_source: | |
| print("📄 Detectat mesaj: 'Accesarea documentelor necesită abonament'") | |
| print("🔍 Caut linkul 'Conectare'...") | |
| try: | |
| # Extrage linkul din regex | |
| import re | |
| match = re.search(r'<a href="(/ro/accounts/login/\?next=.*?)"[^>]*>Conectare</a>', page_source) | |
| if match: | |
| login_path = match.group(1) | |
| # Decodifică URL-ul HTML | |
| from html import unescape | |
| login_path = unescape(login_path) | |
| login_url = f"https://adt.arcanum.com{login_path}" | |
| print(f"✅ Găsit link de conectare: {login_url}") | |
| else: | |
| # Fallback: caută elementul direct | |
| login_link = self.driver.find_element(By.XPATH, | |
| "//a[contains(text(), 'Conectare') and contains(@href, '/accounts/login/')]") | |
| login_url = login_link.get_attribute("href") | |
| print(f"✅ Găsit link prin XPath: {login_url}") | |
| # Navighează la pagina de login | |
| self.driver.get(login_url) | |
| print("✅ Navigat la pagina de login") | |
| time.sleep(2) | |
| except Exception as e: | |
| print(f"⚠ Eroare la găsirea linkului, încerc navigare directă: {e}") | |
| self.driver.get("https://adt.arcanum.com/ro/accounts/login/") | |
| time.sleep(2) | |
| # PASUL 2: Verifică că suntem pe pagina de login | |
| current_url = self.driver.current_url | |
| if "/accounts/login/" not in current_url: | |
| print(f"❌ Nu sunt pe pagina de login! URL curent: {current_url}") | |
| return False | |
| print(f"✅ Sunt pe pagina de login: {current_url}") | |
| # PASUL 3: AȘTEAPTĂ 5 SECUNDE pentru autocomplete | |
| print("⏳ Aștept 5 secunde pentru încărcarea automată a datelor salvate...") | |
| time.sleep(5) | |
| # PASUL 4: Găsește și completează câmpurile | |
| print("🔍 Caut câmpurile de autentificare...") | |
| try: | |
| username_field = self.wait.until( | |
| EC.presence_of_element_located((By.ID, "id_username")) | |
| ) | |
| password_field = self.driver.find_element(By.ID, "id_password") | |
| print("✅ Găsite câmpurile de login") | |
| # Verifică dacă sunt populate automat | |
| current_username = username_field.get_attribute("value") | |
| current_password = password_field.get_attribute("value") | |
| if current_username and current_password: | |
| print(f"✅ Câmpurile sunt deja populate automat!") | |
| print(f" Username: {current_username}") | |
| print(f" Parolă: {'*' * len(current_password)} ({len(current_password)} caractere)") | |
| else: | |
| print("📝 Completez manual credențialele...") | |
| if not current_username: | |
| username_field.clear() | |
| username_field.send_keys("[email protected]") | |
| print("✅ Username completat") | |
| if not current_password: | |
| password_field.clear() | |
| password_field.send_keys("ZCf8Z2Fx54L9v!L") | |
| print("✅ Parolă completată") | |
| # PASUL 5: Așteaptă puțin și apoi submit | |
| time.sleep(1) | |
| print("🔍 Caut butonul de 'Conectare'...") | |
| submit_button = self.driver.find_element( | |
| By.CSS_SELECTOR, | |
| "input.btn.btn-primary[type='submit'][value='Conectare']" | |
| ) | |
| print("✅ Găsit butonul de Conectare") | |
| print("🔐 Trimit formularul de login...") | |
| submit_button.click() | |
| # PASUL 6: Așteaptă finalizarea login-ului | |
| print("⏳ Aștept finalizarea login-ului (5 secunde)...") | |
| time.sleep(5) | |
| # PASUL 7: Verifică succesul login-ului | |
| final_url = self.driver.current_url | |
| if "/accounts/login/" not in final_url: | |
| print("="*60) | |
| print("✅ LOGIN REUȘIT!") | |
| print(f"🔗 Redirecționat către: {final_url}") | |
| print("="*60 + "\n") | |
| return True | |
| else: | |
| # Verifică mesaje de eroare | |
| body_text = self.driver.find_element(By.TAG_NAME, "body").text | |
| print("="*60) | |
| print("❌ LOGIN EȘUAT - Încă pe pagina de login") | |
| if "utilizator" in body_text.lower() or "password" in body_text.lower() or "parolă" in body_text.lower(): | |
| print("⚠ Posibil mesaj de eroare în pagină") | |
| print(f"📄 Fragment din pagină: {body_text[:200]}") | |
| print("="*60 + "\n") | |
| return False | |
| except Exception as e: | |
| print("="*60) | |
| print(f"❌ Eroare la completarea formularului: {e}") | |
| print("="*60 + "\n") | |
| import traceback | |
| traceback.print_exc() | |
| return False | |
| except Exception as e: | |
| print("="*60) | |
| print(f"❌ Eroare generală în perform_auto_login: {e}") | |
| print("="*60 + "\n") | |
| import traceback | |
| traceback.print_exc() | |
| return False | |
| def close_security_popups(self): | |
| """Închide automat pop-up-urile de securitate, DAR NU pagina de limită zilnică""" | |
| try: | |
| print("🔍 Verific dacă s-au deschis pop-up-uri de securitate...") | |
| # Salvează handle-ul ferestrei principale | |
| main_window = self.driver.current_window_handle | |
| all_windows = self.driver.window_handles | |
| # Verifică dacă s-au deschis ferestre noi | |
| if len(all_windows) > 1: | |
| for window in all_windows: | |
| if window != main_window: | |
| try: | |
| # Comută la fereastra nouă | |
| self.driver.switch_to.window(window) | |
| # Verifică dacă e pop-up de securitate | |
| page_text = self.driver.find_element(By.TAG_NAME, "body").text.lower() | |
| current_url = self.driver.current_url | |
| # VERIFICĂ PRIMA DATĂ DACĂ E PAGINA DE LIMITĂ ZILNICĂ - NU O ÎNCHIDE! | |
| daily_limit_indicators = [ | |
| "vezi termeni de utilizare", | |
| "daily download limit reached", | |
| "conditii-de-utilizare", | |
| "terms and conditions" | |
| ] | |
| is_daily_limit = any(indicator in page_text for indicator in daily_limit_indicators) | |
| is_daily_limit_url = "conditii-de-utilizare" in current_url | |
| if is_daily_limit or is_daily_limit_url: | |
| print(f"🛑 DETECTAT PAGINA DE LIMITĂ ZILNICĂ - NU O ÎNCHID!") | |
| print(f"📄 URL: {current_url}") | |
| print(f"📝 Conținut: {page_text[:100]}...") | |
| # NU închide această pagină - lasă scriptul să o detecteze | |
| continue | |
| # Detectează DOAR pop-up-uri de securitate normale (nu limita zilnică) | |
| security_indicators = [ | |
| "let's confirm you are human", | |
| "complete the security check", | |
| "verify that you are not a bot", | |
| "security check", | |
| "javascript is disabled", | |
| "human verification" | |
| ] | |
| # Verifică conținutul pentru pop-up-uri de securitate (dar nu limită zilnică) | |
| is_security_popup = any(indicator in page_text for indicator in security_indicators) | |
| if is_security_popup: | |
| print(f"🔒 Detectat pop-up de securitate în fereastra: {window}") | |
| print(f"📄 URL: {current_url}") | |
| # 🚨 VERIFICARE CRITICĂ: Este CAPTCHA REAL? | |
| # Detectează 2 tipuri de CAPTCHA: | |
| # Tip 1: "Let's confirm you are human" + butonul "amzn-captcha-verify-button" | |
| # Tip 2: "Human Verification" cu "JavaScript is disabled" | |
| try: | |
| page_source = self.driver.page_source | |
| # CAPTCHA Tip 1: Text + Buton | |
| has_captcha_text = ( | |
| '<h1 style="font-weight: normal; color: rgb(221, 107, 16);">Let\'s confirm you are human</h1>' in page_source or | |
| 'Let\'s confirm you are human' in page_source | |
| ) and ('Complete the security check before continuing' in page_source) | |
| has_captcha_button = False | |
| try: | |
| captcha_button = self.driver.find_element(By.ID, "amzn-captcha-verify-button") | |
| if captcha_button: | |
| has_captcha_button = True | |
| except: | |
| pass | |
| # CAPTCHA Tip 2: Human Verification | |
| has_human_verification = ( | |
| '<title>Human Verification</title>' in page_source and | |
| 'JavaScript is disabled' in page_source and | |
| 'you need to verify that you\'re not a robot by solving a CAPTCHA puzzle' in page_source | |
| ) | |
| # CAPTCHA REAL detectat? | |
| if (has_captcha_text and has_captcha_button) or has_human_verification: | |
| captcha_type = "" | |
| if has_captcha_text and has_captcha_button: | |
| captcha_type = "Tip 1: Text + Buton CAPTCHA" | |
| elif has_human_verification: | |
| captcha_type = "Tip 2: Human Verification" | |
| # Determină segment key pentru tracking retry | |
| segment_key = f"{self.current_issue_url}_current_segment" | |
| retry_count = self.captcha_retry_count.get(segment_key, 0) | |
| print(f"\n{'='*60}") | |
| print(f"🚨 CAPTCHA DETECTAT ÎN POP-UP!") | |
| print(f"{'='*60}") | |
| print(f"📋 URL CAPTCHA: {current_url}") | |
| print(f"✅ Tip: {captcha_type}") | |
| print(f"🔢 Detectare #: {retry_count + 1}/{self.captcha_max_retries + 1}") | |
| if retry_count < self.captcha_max_retries: | |
| # DETECTARE cu RETRY - Pauză și reîncearcă | |
| print(f"\n⏸️ DETECTARE CAPTCHA #{retry_count + 1} - PAUZĂ TEMPORARĂ") | |
| print(f"🔄 Strategie: Aștept {self.captcha_wait_minutes} minute și reîncerc") | |
| print(f"💡 Motivație: CAPTCHA expiră după ~{self.captcha_wait_minutes} minute") | |
| print(f"📊 Încercări rămase: {self.captcha_max_retries - retry_count}") | |
| # Marchează retry (incrementează counter-ul) | |
| self.captcha_retry_count[segment_key] = retry_count + 1 | |
| # Închide fereastra cu CAPTCHA | |
| try: | |
| print(f"🚪 Închid fereastra cu CAPTCHA...") | |
| self.driver.close() | |
| if self.driver.window_handles: | |
| self.driver.switch_to.window(self.driver.window_handles[0]) | |
| print(f"✅ Fereastră închisă") | |
| except Exception as e: | |
| print(f"⚠ Eroare la închidere fereastră: {e}") | |
| # Așteaptă 4 minute | |
| wait_seconds = self.captcha_wait_minutes * 60 | |
| print(f"\n⏳ Aștept {self.captcha_wait_minutes} minute ({wait_seconds} secunde)...") | |
| print(f"⏰ Timpul curent: {datetime.now().strftime('%H:%M:%S')}") | |
| for remaining in range(wait_seconds, 0, -30): | |
| mins = remaining // 60 | |
| secs = remaining % 60 | |
| print(f"⏳ Timp rămas: {mins}m {secs}s...") | |
| time.sleep(30) | |
| print(f"⏰ Timpul final: {datetime.now().strftime('%H:%M:%S')}") | |
| print(f"✅ Așteptare completă! Reîncerc segmentul...") | |
| print(f"{'='*60}\n") | |
| # Setează flag pentru retry | |
| self.captcha_retry_needed = True | |
| return # Iese din close_security_popups() | |
| else: | |
| # ULTIMA DETECTARE - Oprește definitiv | |
| print(f"\n🛑 DETECTARE CAPTCHA #{retry_count + 1} - OPRIRE DEFINITIVĂ") | |
| print(f"⚠️ CAPTCHA persistă după {retry_count} retry-uri") | |
| print(f"⚠️ Am așteptat {retry_count * self.captcha_wait_minutes} minute total") | |
| print(f"⚠️ Trebuie intervenție manuală obligatorie!") | |
| # Reset counter | |
| self.captcha_retry_count[segment_key] = 0 | |
| # Salvează starea | |
| print(f"💾 Salvez progresul...") | |
| self.state["captcha_detected"] = True | |
| self.state["captcha_url"] = current_url | |
| self._save_state() | |
| print(f"\n🛑 SCRIPTUL A FOST OPRIT DEFINITIV") | |
| print(f"📋 URL CAPTCHA: {current_url}") | |
| print(f"📋 Progresul salvat în state.json") | |
| print(f"⚠️ ACȚIUNE NECESARĂ: Rezolvă CAPTCHA manual în browser") | |
| print(f"{'='*60}\n") | |
| raise SystemExit("🚨 OPRIRE DEFINITIVĂ - CAPTCHA persistent, intervenție manuală necesară!") | |
| elif has_captcha_text and not has_captcha_button: | |
| print(f"ℹ️ Pop-up cu text similar CAPTCHA dar FĂRĂ butonul CAPTCHA") | |
| print(f"ℹ️ Probabil pagină de download (check-access-save) - nu e CAPTCHA real") | |
| except Exception as e: | |
| print(f"⚠ Eroare la verificarea conținutului CAPTCHA: {e}") | |
| print("⏳ Aștept 2 secunde apoi îl închid...") | |
| time.sleep(5) | |
| # Închide pop-up-ul normal de securitate (non-CAPTCHA) | |
| self.driver.close() | |
| print("✅ Pop-up de securitate închis automat") | |
| else: | |
| print(f"ℹ️ Fereastră nouă detectată dar nu e pop-up de securitate sau e pagină importantă") | |
| except Exception as e: | |
| print(f"⚠ Eroare la verificarea ferestrei {window}: {e}") | |
| # Revine la fereastra principală | |
| try: | |
| self.driver.switch_to.window(main_window) | |
| except: | |
| if self.driver.window_handles: | |
| self.driver.switch_to.window(self.driver.window_handles[0]) | |
| except Exception as e: | |
| print(f"⚠ Eroare în close_security_popups: {e}") | |
| def _has_normal_html_structure(self, page_source): | |
| """ | |
| FIXED: Verifică dacă pagina are structura HTML normală specifică site-ului Arcanum. | |
| IMPORTANT: Chrome page_source nu întotdeauna include DOCTYPE, deci nu ne bazăm pe el! | |
| """ | |
| if not page_source: | |
| return False | |
| # Normalizează spațiile și new lines pentru verificare | |
| normalized_source = ' '.join(page_source.strip().split()) | |
| normalized_start = normalized_source[:500].lower() | |
| # INDICATORI POZITIVI pentru pagini normale Arcanum | |
| normal_indicators = [ | |
| 'html lang="ro"', # Toate paginile Arcanum au asta | |
| '<title>', # Toate au titlu | |
| '<head>', # Toate au head | |
| 'ziarele arcanum', # În titlu | |
| 'meta charset="utf-8"', # Meta charset standard | |
| 'meta name="viewport"' # Meta viewport standard | |
| ] | |
| # INDICATORI NEGATIVI pentru pagini de limită/eroare | |
| limit_indicators = [ | |
| 'vezi', # "Vezi Termeni de utilizare" | |
| 'conditii-de-utilizare', # URL sau link către condiții | |
| 'daily download limit', # Vechiul mesaj | |
| 'terms and conditions' # Versiunea engleză | |
| ] | |
| # Contorizează indicatorii pozitivi | |
| positive_count = sum(1 for indicator in normal_indicators | |
| if indicator in normalized_start) | |
| # Contorizează indicatorii negativi | |
| negative_count = sum(1 for indicator in limit_indicators | |
| if indicator in normalized_start) | |
| # Verifică dimensiunea - paginile de limită sunt foarte mici | |
| is_too_small = len(normalized_source) < 300 | |
| # LOGICA DE DECIZIE: | |
| # 1. Dacă are indicatori negativi ȘI e mică → pagină de limită | |
| if negative_count > 0 and is_too_small: | |
| print(f"🚨 PAGINĂ DE LIMITĂ detectată:") | |
| print(f" Indicatori negativi: {negative_count}") | |
| print(f" Dimensiune: {len(normalized_source)} chars") | |
| print(f" Conținut: '{normalized_source[:200]}'") | |
| return False | |
| # 2. Dacă are suficienți indicatori pozitivi → pagină normală | |
| if positive_count >= 4: # Cel puțin 4 din 6 indicatori pozitivi | |
| return True | |
| # 3. Dacă e foarte mică și fără indicatori pozitivi → suspicioasă | |
| if is_too_small and positive_count < 2: | |
| print(f"🔍 PAGINĂ SUSPICIOASĂ (prea mică și fără indicatori):") | |
| print(f" Indicatori pozitivi: {positive_count}/6") | |
| print(f" Dimensiune: {len(normalized_source)} chars") | |
| print(f" Conținut: '{normalized_source[:200]}'") | |
| return False | |
| # 4. În toate celelalte cazuri → consideră normală | |
| return True | |
| def _is_suspicious_page(self, body_text, url, page_source): | |
| """ | |
| FIXED: Helper mai inteligent pentru a determina dacă o pagină merită debugging | |
| EXCLUDERE EXPLICITĂ pentru about:blank și alte file normale de browser | |
| """ | |
| # EXCLUDERE EXPLICITĂ pentru about:blank și alte file normale de browser | |
| if url == "about:blank" or "about:blank" in url: | |
| return False # Nu e suspicioasă - e pagină normală de browser | |
| # Exclude și alte URL-uri normale de Chrome | |
| if url.startswith("chrome://") or url.startswith("chrome-extension://"): | |
| return False | |
| # Exclude URL-urile goale sau None | |
| if not url or url.strip() == "": | |
| return False | |
| # Indicatori clari de pagini problematice | |
| clear_limit_signs = [ | |
| "Vezi" in body_text and len(body_text) < 200, | |
| "conditii" in body_text.lower(), | |
| "limit" in body_text.lower() and len(body_text) < 500, | |
| "daily download" in body_text.lower() | |
| ] | |
| # Pagini foarte mici sunt întotdeauna suspicioase DOAR dacă nu sunt about:blank | |
| too_small = len(body_text.strip()) < 100 | |
| # NU detecta ca suspicioase paginile normale mari | |
| is_normal_arcanum = ( | |
| len(body_text) > 500 and | |
| "Analele" in body_text and | |
| ("Universității" in body_text or "Matematică" in body_text) | |
| ) | |
| if is_normal_arcanum: | |
| return False # Nu e suspicioasă - e pagină normală Arcanum | |
| return any(clear_limit_signs) or too_small | |
| def save_page_range(self, start, end, retries=1): | |
| """FIXED: Verifică URL-ul + verifică limita zilnică + verifică login + închide pop-up-urile automat + retry după CAPTCHA""" | |
| for attempt in range(1, retries + 2): | |
| print(f"🔄 Încep segmentul {start}-{end}, încercarea {attempt}") | |
| # VERIFICARE 1: Suntem pe documentul corect? | |
| try: | |
| current_url = self.driver.current_url | |
| if self.current_issue_url not in current_url: | |
| print(f"🚨 EROARE: Browser-ul a navigat la URL greșit!") | |
| print(f" Așteptat: {self.current_issue_url}") | |
| print(f" Actual: {current_url}") | |
| print(f"🔄 Renavigez la documentul corect...") | |
| if not self.navigate_to_page(self.current_issue_url): | |
| print(f"❌ Nu pot renaviga la {self.current_issue_url}") | |
| return False | |
| time.sleep(3) | |
| print(f"✅ Renavigat cu succes la documentul corect") | |
| except Exception as e: | |
| print(f"⚠ Eroare la verificarea URL-ului: {e}") | |
| # VERIFICARE 2: Este nevoie de login? | |
| if self.detect_login_required(): | |
| print("🔐 DETECTAT: Este necesară autentificarea!") | |
| print("🔄 Încerc login automat...") | |
| login_success = self.perform_auto_login() | |
| if not login_success: | |
| print("❌ LOGIN EȘUAT - Opresc descărcarea") | |
| print("⏸️ PAUZĂ NECESARĂ - Verifică manual credențialele!") | |
| return False | |
| print("✅ Login reușit - renavigez la document...") | |
| # După login, renavigăm la documentul original | |
| if not self.navigate_to_page(self.current_issue_url): | |
| print(f"❌ Nu pot renaviga la {self.current_issue_url} după login") | |
| return False | |
| time.sleep(3) | |
| print("✅ Renavigat cu succes după login") | |
| # 🔧 VERIFICARE 1: MENTENANȚĂ (403 Forbidden) | |
| if self.detect_403_maintenance(): | |
| print(f"⚠️ Detectat 403 la segmentul {start}-{end} - Arcanum în mentenanță") | |
| # Așteaptă finalul mentenanței | |
| if self.wait_for_maintenance(wait_minutes=10, max_retries=3): | |
| print(f"✅ Mentenanță finalizată - reîncerc segmentul {start}-{end}") | |
| # Renavigăm la documentul corect după mentenanță | |
| if not self.navigate_to_page(self.current_issue_url): | |
| print(f"❌ Nu pot renaviga după mentenanță") | |
| return False | |
| time.sleep(3) | |
| # Continuă cu încercarea curentă | |
| else: | |
| print(f"❌ Mentenanță prea lungă - abandonez segmentul {start}-{end}") | |
| return False | |
| # 🚨 VERIFICARE 2: CAPTCHA | |
| if self.detect_captcha(): | |
| print(f"\n{'='*60}") | |
| print(f"🚨🚨🚨 CAPTCHA DETECTAT ÎN TIMPUL DESCĂRCĂRII! 🚨🚨🚨") | |
| print(f"{'='*60}") | |
| print(f"❌ CAPTCHA detectat la segmentul {start}-{end}") | |
| print(f"💾 Salvez progresul și opresc scriptul...") | |
| self.state["captcha_detected"] = True | |
| self.state["captcha_url"] = self.driver.current_url | |
| self._save_state() | |
| raise SystemExit("🚨 OPRIRE CAPTCHA - Verificare umană necesară!") | |
| # Continuă cu logica existentă... | |
| if not self.open_save_popup(): | |
| print(f"⚠ Eșec la deschiderea popup-ului pentru {start}-{end}") | |
| time.sleep(2) | |
| continue | |
| success = self.fill_and_save_range(start, end) | |
| if success: | |
| print("⏳ Aștept 5s pentru inițierea descărcării...") | |
| time.sleep(5) | |
| # Închide automat pop-up-urile de securitate | |
| self.captcha_retry_needed = False # Reset flag | |
| self.close_security_popups() | |
| # Verifică dacă trebuie retry după CAPTCHA | |
| if self.captcha_retry_needed: | |
| print(f"🔄 CAPTCHA retry flag detectat - reîncerc segmentul {start}-{end}") | |
| continue # Reîncearcă segmentul | |
| print("⏳ Aștept 5s pentru finalizarea descărcării segmentului...") | |
| time.sleep(5) | |
| # Verifică limita zilnică IMEDIAT DUPĂ descărcare | |
| if self.check_for_daily_limit_popup(): | |
| print(f"🛑 OPRIRE INSTANT - Limită zilnică detectată după segmentul {start}-{end}") | |
| return False | |
| print(f"✅ Segmentul {start}-{end} descărcat cu succes") | |
| # Resetează counter-ul CAPTCHA pentru acest segment (dacă exista) | |
| segment_key = f"{self.current_issue_url}_current_segment" | |
| if segment_key in self.captcha_retry_count: | |
| print(f"✅ Reset counter CAPTCHA pentru segment (era la {self.captcha_retry_count[segment_key]})") | |
| self.captcha_retry_count[segment_key] = 0 | |
| return True | |
| else: | |
| print(f"⚠ Retry pentru segmentul {start}-{end}") | |
| time.sleep(2) | |
| print(f"❌ Renunț la segmentul {start}-{end} după {retries+1} încercări.") | |
| return False | |
| def save_all_pages_in_batches(self, resume_from=1): | |
| """FIXED: Refă segmentele incomplete în loc să continue din mijloc""" | |
| total = self.get_total_pages() | |
| if total <= 0: | |
| print("⚠ Nu am obținut numărul total de pagini; nu pot continua.") | |
| return 0, False | |
| print(f"🎯 TOTAL PAGINI DETECTAT: {total}") | |
| bs = self.batch_size # 50 | |
| # PASUL 1: Calculează segmentele standard | |
| all_segments = [] | |
| # Primul segment: 1 până la bs-1 (1-49) | |
| first_end = min(bs - 1, total) | |
| if first_end >= 1: | |
| all_segments.append((1, first_end)) | |
| # Segmentele următoare: bs, bs*2-1, etc. (50-99, 100-149, etc.) | |
| current = bs | |
| while current <= total: | |
| end = min(current + bs - 1, total) | |
| all_segments.append((current, end)) | |
| current += bs | |
| print(f"📊 SEGMENTE STANDARD CALCULATE: {len(all_segments)}") | |
| for i, (start, end) in enumerate(all_segments): | |
| print(f" {i+1}. Segment {start}-{end}") | |
| # PASUL 2: Verifică ce segmente sunt COMPLET descărcate pe disk | |
| completed_segments = [] | |
| for i, (seg_start, seg_end) in enumerate(all_segments): | |
| # Verifică dacă există un fișier care acoperă COMPLET segmentul | |
| segments_on_disk = self.get_all_pdf_segments_for_issue(self.current_issue_url) | |
| segment_complete = False | |
| for disk_seg in segments_on_disk: | |
| disk_start = disk_seg['start'] | |
| disk_end = disk_seg['end'] | |
| # Verifică dacă segmentul de pe disk acoperă COMPLET segmentul standard | |
| if disk_start <= seg_start and disk_end >= seg_end: | |
| segment_complete = True | |
| print(f"✅ Segment {i+1} ({seg_start}-{seg_end}) COMPLET pe disk: {disk_seg['filename']}") | |
| break | |
| if segment_complete: | |
| completed_segments.append(i) | |
| else: | |
| # Verifică dacă există fișiere parțiale pentru acest segment | |
| partial_files = [seg for seg in segments_on_disk | |
| if seg['start'] >= seg_start and seg['end'] <= seg_end] | |
| if partial_files: | |
| print(f"⚠ Segment {i+1} ({seg_start}-{seg_end}) PARȚIAL pe disk:") | |
| for pf in partial_files: | |
| print(f" 📄 {pf['filename']} (pagini {pf['start']}-{pf['end']}) - VA FI REFĂCUT") | |
| else: | |
| print(f"🆕 Segment {i+1} ({seg_start}-{seg_end}) lipsește complet") | |
| # PASUL 3: Începe cu primul segment incomplet | |
| start_segment_index = 0 | |
| for i in range(len(all_segments)): | |
| if i not in completed_segments: | |
| start_segment_index = i | |
| break | |
| else: | |
| # Toate segmentele sunt complete | |
| print("✅ Toate segmentele sunt complete pe disk!") | |
| return total, False | |
| print(f"🎯 ÎNCEP cu segmentul {start_segment_index + 1} (primul incomplet)") | |
| # PASUL 4: Procesează segmentele începând cu primul incomplet | |
| segments_to_process = all_segments[start_segment_index:] | |
| print(f"🎯 PROCESEZ {len(segments_to_process)} segmente începând cu segmentul {start_segment_index + 1}") | |
| # PASUL 5: ȘTERGE fișierele parțiale pentru segmentele care vor fi refăcute | |
| for i, (seg_start, seg_end) in enumerate(segments_to_process): | |
| actual_index = start_segment_index + i | |
| if actual_index not in completed_segments: | |
| # Șterge fișierele parțiale pentru acest segment | |
| segments_on_disk = self.get_all_pdf_segments_for_issue(self.current_issue_url) | |
| for disk_seg in segments_on_disk: | |
| if disk_seg['start'] >= seg_start and disk_seg['end'] <= seg_end: | |
| try: | |
| os.remove(disk_seg['path']) | |
| print(f"🗑️ ȘTERG fișier parțial: {disk_seg['filename']}") | |
| except Exception as e: | |
| print(f"⚠ Nu am putut șterge {disk_seg['filename']}: {e}") | |
| # PASUL 5.5: RE-SCANEAZĂ disk-ul DUPĂ ștergere pentru a vedea ce există ACUM | |
| print(f"\n🔍 RE-SCANEZ disk-ul după ștergerea fișierelor parțiale...") | |
| segments_on_disk_now = self.get_all_pdf_segments_for_issue(self.current_issue_url) | |
| # Re-calculează segmentele complete ACUM (după ștergere) | |
| completed_segments_now = [] | |
| for i, (seg_start, seg_end) in enumerate(all_segments): | |
| segment_complete = False | |
| for disk_seg in segments_on_disk_now: | |
| disk_start = disk_seg['start'] | |
| disk_end = disk_seg['end'] | |
| if disk_start <= seg_start and disk_end >= seg_end: | |
| segment_complete = True | |
| completed_segments_now.append(i) | |
| print(f"✅ Segment {i+1} ({seg_start}-{seg_end}) EXISTĂ ACUM pe disk: {disk_seg['filename']}") | |
| break | |
| if not segment_complete: | |
| print(f"❌ Segment {i+1} ({seg_start}-{seg_end}) LIPSEȘTE - va fi descărcat") | |
| # Re-calculează segments_to_process bazat pe ce există ACUM | |
| actual_segments_to_download = [] | |
| for i in range(len(all_segments)): | |
| if i not in completed_segments_now: | |
| actual_segments_to_download.append(all_segments[i]) | |
| if not actual_segments_to_download: | |
| print("✅ Toate segmentele sunt complete după re-scanare!") | |
| return total, False | |
| print(f"\n🎯 După re-scanare: trebuie să descarc {len(actual_segments_to_download)} segmente lipsă") | |
| for seg_start, seg_end in actual_segments_to_download[:5]: # Afișează primele 5 | |
| print(f" 📥 Segment de descărcat: {seg_start}-{seg_end}") | |
| if len(actual_segments_to_download) > 5: | |
| print(f" ... și încă {len(actual_segments_to_download) - 5} segmente") | |
| # Calculează last_successful_page bazat pe ultimul segment complet | |
| last_successful_page = 0 | |
| if completed_segments_now: | |
| max_completed_index = max(completed_segments_now) | |
| last_successful_page = all_segments[max_completed_index][1] | |
| print(f"📊 Ultimul segment complet: index {max_completed_index}, pagina {last_successful_page}") | |
| # PASUL 6: Descarcă DOAR segmentele care lipsesc ACUM | |
| failed_segments = [] | |
| consecutive_failures = 0 | |
| MAX_CONSECUTIVE_FAILURES = 3 | |
| for i, (start, end) in enumerate(actual_segments_to_download): | |
| print(f"📦 Procesez segmentul LIPSĂ {start}-{end} ({i+1}/{len(actual_segments_to_download)})") | |
| # Încercă să descarce segmentul cu retry | |
| result = self.save_page_range(start, end, retries=3) | |
| if not result: | |
| if self.state.get("daily_limit_hit", False): | |
| print(f"🛑 OPRIRE - Limită zilnică atinsă la segmentul {start}-{end}") | |
| return last_successful_page, True | |
| print(f"❌ SEGMENT EȘUAT: {start}-{end}") | |
| failed_segments.append((start, end)) | |
| consecutive_failures += 1 | |
| if consecutive_failures >= MAX_CONSECUTIVE_FAILURES: | |
| print(f"🚨 PREA MULTE EȘECURI CONSECUTIVE ({consecutive_failures})") | |
| print(f"🔄 ÎNCERC RECOVERY COMPLET...") | |
| try: | |
| if not self.setup_chrome_driver(): | |
| print(f"❌ Recovery eșuat - opresc procesarea") | |
| break | |
| if not self.navigate_to_page(self.current_issue_url): | |
| print(f"❌ Nu pot renaviga după recovery") | |
| break | |
| consecutive_failures = 0 | |
| print(f"✅ Recovery reușit - REÎNCERC segmentul eșuat {start}-{end}") | |
| time.sleep(5) | |
| # 🔥 REÎNCEARCĂ SEGMENTUL EȘUAT în loc să sară peste el | |
| print(f"🔄 REÎNCERC: Segmentul {start}-{end} după recovery...") | |
| retry_result = self.save_page_range(start, end, retries=3) | |
| if retry_result: | |
| print(f"✅ SUCCESS după recovery: Segmentul {start}-{end}") | |
| # Elimină din failed_segments dacă reușește | |
| if (start, end) in failed_segments: | |
| failed_segments.remove((start, end)) | |
| # Actualizează progresul | |
| last_successful_page = end | |
| self._update_partial_issue_progress(self.current_issue_url, end, total_pages=total) | |
| print(f"✅ Progres salvat: pagini până la {end}") | |
| else: | |
| print(f"❌ Segmentul {start}-{end} a eșuat din nou după recovery") | |
| print(f"⏭️ Continui cu următorul segment...") | |
| except Exception as e: | |
| print(f"❌ Eroare în recovery: {e}") | |
| break | |
| else: | |
| print(f"🔄 Eșecuri consecutive: {consecutive_failures}/{MAX_CONSECUTIVE_FAILURES}") | |
| print(f"⏳ Continui cu următorul segment după pauză...") | |
| time.sleep(2) | |
| else: | |
| # SEGMENT REUȘIT | |
| consecutive_failures = 0 | |
| last_successful_page = end | |
| self._update_partial_issue_progress(self.current_issue_url, end, total_pages=total) | |
| print(f"✅ Progres salvat: pagini până la {end}") | |
| time.sleep(1) | |
| # RAPORTARE FINALĂ | |
| successful_segments = len(actual_segments_to_download) - len(failed_segments) | |
| print(f"📊 PROGRES FINAL: {last_successful_page}/{total} pagini") | |
| print(f"📊 SEGMENTE: {successful_segments}/{len(actual_segments_to_download)} reușite") | |
| if failed_segments: | |
| print(f"📊 SEGMENTE EȘUATE: {len(failed_segments)}") | |
| for start, end in failed_segments: | |
| print(f" ❌ {start}-{end}") | |
| # === VERIFICARE CRITICĂ FINALĂ: SCANEAZĂ DISK-UL PENTRU PROGRES REAL === | |
| print(f"\n🔍 VERIFICARE CRITICĂ: Scanez disk-ul pentru progres REAL...") | |
| actual_segments_on_disk = self.get_all_pdf_segments_for_issue(self.current_issue_url) | |
| if actual_segments_on_disk: | |
| print(f"📄 Segmente găsite pe disk: {len(actual_segments_on_disk)}") | |
| # Sortează segmentele după pagina de început | |
| actual_segments_on_disk.sort(key=lambda x: x['start']) | |
| # Afișează segmentele găsite | |
| for seg in actual_segments_on_disk: | |
| print(f" ✅ {seg['filename']} (pagini {seg['start']}-{seg['end']})") | |
| # === CALCUL CORECT: Găsește ultimul segment CONSECUTIV de la început === | |
| real_last_page = 0 | |
| # Verifică fiecare segment așteptat în ordine | |
| for i, (expected_start, expected_end) in enumerate(all_segments): | |
| # Caută dacă acest segment există pe disk | |
| found = False | |
| for disk_seg in actual_segments_on_disk: | |
| if disk_seg['start'] <= expected_start and disk_seg['end'] >= expected_end: | |
| found = True | |
| real_last_page = expected_end # Actualizează progresul | |
| break | |
| # Dacă lipsește un segment, OPREȘTE numărarea | |
| if not found: | |
| print(f"⚠️ OPRIT LA SEGMENT: {expected_start}-{expected_end} (LIPSEȘTE)") | |
| break | |
| print(f"📊 PROGRES REAL CONSECUTIV DE PE DISK: {real_last_page}/{total} pagini") | |
| # Identifică TOATE găurile (nu doar până la primul lipsă) | |
| missing_ranges = [] | |
| for i, (expected_start, expected_end) in enumerate(all_segments): | |
| found = False | |
| for disk_seg in actual_segments_on_disk: | |
| if disk_seg['start'] <= expected_start and disk_seg['end'] >= expected_end: | |
| found = True | |
| break | |
| if not found: | |
| missing_ranges.append((expected_start, expected_end)) | |
| if missing_ranges: | |
| print(f"\n⚠️ GĂURI DETECTATE: {len(missing_ranges)} segmente lipsă pe disk!") | |
| for start, end in missing_ranges[:10]: # Primele 10 | |
| print(f" ❌ LIPSEȘTE: pages{start}-{end}") | |
| if len(missing_ranges) > 10: | |
| print(f" ... și încă {len(missing_ranges) - 10} segmente lipsă") | |
| # FOLOSEȘTE PROGRESUL REAL CONSECUTIV DE PE DISK | |
| last_successful_page = real_last_page | |
| print(f"\n✅ PROGRES FINAL CORECTAT (CONSECUTIV): {last_successful_page}/{total}") | |
| else: | |
| print(f"⚠️ Nu am găsit NICIUN segment pe disk pentru acest issue!") | |
| last_successful_page = 0 | |
| completion_rate = (last_successful_page / total) * 100 if total > 0 else 0 | |
| is_complete = completion_rate >= 98 and len(failed_segments) == 0 | |
| return last_successful_page, False | |
| def verify_all_segments_present(self, issue_url, total_pages): | |
| """ | |
| CRITICAL: Verifică că TOATE segmentele sunt prezente și consecutive | |
| Returns: (bool: all_present, list: missing_segments) | |
| """ | |
| print(f"🔍 VERIFICARE CRITICĂ: Scanez completitudinea segmentelor pentru {issue_url}") | |
| # PASUL 1: Obține toate segmentele de pe disk | |
| all_segments = self.get_all_pdf_segments_for_issue(issue_url) | |
| if not all_segments: | |
| print(f"❌ Nu am găsit niciun segment!") | |
| return False, [] | |
| # PASUL 2: Calculează segmentele așteptate | |
| bs = self.batch_size # 50 | |
| expected_segments = [] | |
| # Primul segment: 1-49 | |
| first_end = min(bs - 1, total_pages) | |
| if first_end >= 1: | |
| expected_segments.append((1, first_end)) | |
| # Segmentele următoare: 50-99, 100-149, etc. | |
| current = bs | |
| while current <= total_pages: | |
| end = min(current + bs - 1, total_pages) | |
| expected_segments.append((current, end)) | |
| current += bs | |
| print(f"📊 Segmente așteptate: {len(expected_segments)}") | |
| # PASUL 3: Verifică fiecare segment așteptat | |
| missing_segments = [] | |
| for expected_start, expected_end in expected_segments: | |
| # Caută un segment care acoperă complet intervalul așteptat | |
| found = False | |
| for disk_seg in all_segments: | |
| disk_start = disk_seg['start'] | |
| disk_end = disk_seg['end'] | |
| # Verifică dacă segmentul de pe disk acoperă complet intervalul așteptat | |
| if disk_start <= expected_start and disk_end >= expected_end: | |
| found = True | |
| print(f" ✅ Segment {expected_start}-{expected_end}: găsit ({disk_seg['filename']})") | |
| break | |
| if not found: | |
| missing_segments.append((expected_start, expected_end)) | |
| print(f" ❌ Segment {expected_start}-{expected_end}: LIPSEȘTE!") | |
| # PASUL 4: Raport final | |
| if missing_segments: | |
| print(f"🚨 PROBLEMA DETECTATĂ: {len(missing_segments)} segmente lipsă!") | |
| for start, end in missing_segments: | |
| print(f" 📄 Lipsește: pages{start}-{end}") | |
| return False, missing_segments | |
| else: | |
| print(f"✅ TOATE {len(expected_segments)} segmente sunt prezente și complete!") | |
| return True, [] | |
| def download_missing_segments(self, issue_url, missing_segments): | |
| """ | |
| Descarcă segmentele lipsă pentru un issue incomplet | |
| """ | |
| if not missing_segments: | |
| return True | |
| print(f"🔄 RECUPERARE: Descarc {len(missing_segments)} segmente lipsă pentru {issue_url}") | |
| # PASUL 1: Navigă la issue | |
| if not self.navigate_to_page(issue_url): | |
| print(f"❌ Nu pot naviga la {issue_url}") | |
| return False | |
| time.sleep(3) | |
| # PASUL 2: Descarcă fiecare segment lipsă | |
| success_count = 0 | |
| for start, end in missing_segments: | |
| print(f"📥 Descarc segmentul lipsă: pages{start}-{end}") | |
| result = self.save_page_range(start, end, retries=3) | |
| if result: | |
| success_count += 1 | |
| print(f"✅ Segment recuperat: pages{start}-{end}") | |
| else: | |
| print(f"❌ Nu am putut recupera segmentul: pages{start}-{end}") | |
| # Verifică limita zilnică | |
| if self.state.get("daily_limit_hit", False): | |
| print(f"🛑 Limită zilnică atinsă în timpul recuperării") | |
| return False | |
| time.sleep(2) | |
| print(f"📊 Recuperare finalizată: {success_count}/{len(missing_segments)} segmente descărcate") | |
| return success_count == len(missing_segments) | |
| def extract_issue_links_from_collection(self): | |
| """ | |
| FIXED: Extrage toate linkurile de issue din colecție, inclusiv pentru limba ungară | |
| Folosește selector generic pentru a detecta orice limbă (/view/, /ro/view/, /en/view/, /hu/view/) | |
| """ | |
| try: | |
| # Așteaptă ca lista să se încarce | |
| self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'ul.list-group'))) | |
| # SELECTOR GENERIC: orice link care conține '/view/' în href | |
| anchors = self.driver.find_elements(By.CSS_SELECTOR, 'li.list-group-item a[href*="/view/"]') | |
| print(f"🔍 Am găsit {len(anchors)} linkuri brute cu '/view/' în colecție") | |
| links = [] | |
| for a in anchors: | |
| href = a.get_attribute("href") | |
| if href and '/view/' in href: | |
| # Normalizează URL-ul (elimină parametrii și slash-ul final) | |
| normalized = href.split('?')[0].rstrip('/') | |
| links.append(normalized) | |
| # Elimină dublurile păstrând ordinea | |
| unique = [] | |
| seen = set() | |
| for l in links: | |
| if l not in seen: | |
| seen.add(l) | |
| unique.append(l) | |
| print(f"🔗 Am găsit {len(unique)} linkuri UNICE de issue în colecție") | |
| # DEBUGGING pentru colecțiile problematice | |
| if len(unique) == 0: | |
| print(f"🔍 DEBUG - Nu am găsit linkuri. Analizez structura paginii...") | |
| # Verifică dacă există lista de grupuri | |
| try: | |
| list_groups = self.driver.find_elements(By.CSS_SELECTOR, 'ul.list-group') | |
| print(f"🔍 Liste grup găsite: {len(list_groups)}") | |
| list_items = self.driver.find_elements(By.CSS_SELECTOR, 'li.list-group-item') | |
| print(f"🔍 Elemente listă găsite: {len(list_items)}") | |
| # Verifică toate linkurile din pagină | |
| all_links = self.driver.find_elements(By.CSS_SELECTOR, 'a[href*="/view/"]') | |
| print(f"🔍 TOATE linkurile cu '/view/' din pagină: {len(all_links)}") | |
| for i, link in enumerate(all_links[:10]): # Primele 10 pentru debugging | |
| href = link.get_attribute("href") | |
| text = link.text.strip()[:50] | |
| print(f" {i+1}. 🔗 {href}") | |
| print(f" 📝 Text: '{text}'") | |
| # Verifică structura HTML a primelor elemente | |
| if list_items: | |
| print(f"🔍 Primul element listă HTML:") | |
| print(f" {list_items[0].get_attribute('outerHTML')[:200]}...") | |
| except Exception as debug_e: | |
| print(f"⚠ Eroare în debugging: {debug_e}") | |
| else: | |
| # Afișează primele câteva linkuri găsite pentru verificare | |
| print(f"📋 Primele linkuri găsite:") | |
| for i, link in enumerate(unique[:5]): | |
| # Extrage anul sau identificatorul din URL | |
| parts = link.split('/')[-1].split('_') | |
| identifier = parts[-1] if len(parts) > 1 else "N/A" | |
| print(f" {i+1}. 🔗 {identifier}: {link}") | |
| if len(unique) > 5: | |
| print(f" 📊 ... și încă {len(unique) - 5} linkuri") | |
| return unique | |
| except Exception as e: | |
| print(f"❌ Eroare la extragerea linkurilor din colecție: {e}") | |
| # Debugging suplimentar în caz de eroare | |
| try: | |
| current_url = self.driver.current_url | |
| page_title = self.driver.title | |
| print(f"🔍 URL curent: {current_url}") | |
| print(f"🔍 Titlu pagină: {page_title}") | |
| # Verifică dacă pagina s-a încărcat corect | |
| body_text = self.driver.find_element(By.TAG_NAME, "body").text[:200] | |
| print(f"🔍 Început conținut: '{body_text}...'") | |
| except Exception as debug_e: | |
| print(f"⚠ Eroare în debugging după eroare: {debug_e}") | |
| return [] | |
| def extract_page_range_from_filename(self, filename): | |
| """Extrage range-ul de pagini din numele fișierului pentru sortare corectă""" | |
| match = re.search(r'__pages(\d+)-(\d+)\.pdf', filename) | |
| if match: | |
| start_page = int(match.group(1)) | |
| end_page = int(match.group(2)) | |
| return (start_page, end_page) | |
| return (0, 0) | |
| def copy_and_combine_issue_pdfs(self, issue_url: str, issue_title: str): | |
| """ | |
| FIXED: MUTĂ fișierele în folder și le combină (nu mai păstrează pe D:) | |
| ADDED: Face backup în g:Temporare înainte de procesare | |
| """ | |
| issue_id = issue_url.rstrip('/').split('/')[-1] | |
| folder_name = self._safe_folder_name(issue_title or issue_id) | |
| dest_dir = os.path.join(self.download_dir, folder_name) | |
| os.makedirs(dest_dir, exist_ok=True) | |
| # DIRECTORUL DE BACKUP | |
| backup_base_dir = r"g:\Temporare" | |
| backup_dir = os.path.join(backup_base_dir, folder_name) | |
| print(f"📁 Procesez PDF-urile pentru '{issue_title}' cu ID '{issue_id}'") | |
| # ⏳ AȘTEAPTĂ CA TOATE FIȘIERELE SĂ FIE COMPLET DESCĂRCATE | |
| print("⏳ Aștept 10 secunde ca toate fișierele să se termine de descărcat...") | |
| time.sleep(20) | |
| # PASUL 1: Găsește TOATE fișierele pentru acest issue | |
| all_segments = self.get_all_pdf_segments_for_issue(issue_url) | |
| if not all_segments: | |
| print(f"ℹ️ Nu am găsit fișiere PDF pentru '{issue_title}' cu ID '{issue_id}'.") | |
| return | |
| print(f"🔍 Am găsit {len(all_segments)} fișiere PDF pentru '{issue_id}':") | |
| for seg in all_segments: | |
| print(f" 📄 {seg['filename']} (pagini {seg['start']}-{seg['end']})") | |
| # PASUL 1.5: CREEAZĂ BACKUP-UL ÎNAINTE DE PROCESARE | |
| print(f"💾 Creez backup în: {backup_dir}") | |
| try: | |
| os.makedirs(backup_dir, exist_ok=True) | |
| backup_success = True | |
| backup_size_total = 0 | |
| for seg in all_segments: | |
| src = seg['path'] | |
| backup_dst = os.path.join(backup_dir, seg['filename']) | |
| try: | |
| shutil.copy2(src, backup_dst) # copy2 păstrează și metadata | |
| file_size = os.path.getsize(backup_dst) | |
| backup_size_total += file_size | |
| print(f"💾 BACKUP: {seg['filename']} → g:\\Temporare\\{folder_name}\\") | |
| except Exception as e: | |
| print(f"⚠ EROARE backup pentru {seg['filename']}: {e}") | |
| backup_success = False | |
| backup_size_mb = backup_size_total / (1024 * 1024) | |
| if backup_success: | |
| print(f"✅ BACKUP COMPLET: {len(all_segments)} fișiere ({backup_size_mb:.2f} MB) în {backup_dir}") | |
| else: | |
| print(f"⚠ BACKUP PARȚIAL: Unele fișiere nu au putut fi copiate în backup") | |
| except Exception as e: | |
| print(f"❌ EROARE la crearea backup-ului: {e}") | |
| print(f"🛡️ OPRESC PROCESAREA pentru siguranță - fișierele rămân pe G:\\") | |
| return | |
| # PASUL 2: MUTĂ (nu copiază) TOATE fișierele în folder (DOAR DUPĂ backup SUCCESS) | |
| moved_files = [] | |
| for seg in all_segments: | |
| src = seg['path'] | |
| dst = os.path.join(dest_dir, seg['filename']) | |
| try: | |
| shutil.move(src, dst) # MOVE în loc de COPY | |
| moved_files.append(dst) | |
| print(f"📄 MUTAT: {seg['filename']} → {folder_name}/") | |
| except Exception as e: | |
| print(f"⚠ Nu am reușit să mut {seg['filename']}: {e}") | |
| if not moved_files: | |
| print(f"❌ Nu am reușit să mut niciun fișier pentru '{issue_title}'.") | |
| return | |
| print(f"📁 Toate {len(moved_files)} PDF-urile pentru '{issue_title}' au fost MUTATE în '{dest_dir}'.") | |
| print(f"💾 BACKUP SIGUR găsit în: {backup_dir}") | |
| # PASUL 3: Combină PDF-urile în ordinea corectă | |
| output_file = os.path.join(dest_dir, f"{folder_name}.pdf") | |
| try: | |
| if len(moved_files) > 1: | |
| print(f"🔗 Combinez {len(moved_files)} fișiere PDF în ordinea corectă...") | |
| # Sortează fișierele după range-ul de pagini | |
| files_with_ranges = [] | |
| for file_path in moved_files: | |
| filename = os.path.basename(file_path) | |
| start_page, end_page = self.extract_page_range_from_filename(filename) | |
| files_with_ranges.append((start_page, end_page, file_path)) | |
| # Sortează după pagina de început | |
| files_with_ranges.sort(key=lambda x: x[0]) | |
| sorted_files = [x[2] for x in files_with_ranges] | |
| # Afișează ordinea de combinare | |
| print("📋 Ordinea de combinare:") | |
| for start, end, path in files_with_ranges: | |
| filename = os.path.basename(path) | |
| print(f" 📄 {filename} (pagini {start}-{end})") | |
| from PyPDF2 import PdfMerger | |
| merger = PdfMerger() | |
| for pdf_path in sorted_files: | |
| try: | |
| merger.append(pdf_path) | |
| filename = os.path.basename(pdf_path) | |
| print(f" ✅ Adăugat în ordine: {filename}") | |
| except Exception as e: | |
| print(f" ⚠ Eroare la adăugarea {pdf_path}: {e}") | |
| # Scrie fișierul combinat | |
| merger.write(output_file) | |
| merger.close() | |
| # Verifică că fișierul combinat a fost creat cu succes | |
| if os.path.exists(output_file) and os.path.getsize(output_file) > 0: | |
| file_size_mb = os.path.getsize(output_file) / (1024 * 1024) | |
| print(f"✅ Fișierul combinat creat cu succes: {file_size_mb:.2f} MB") | |
| # ȘTERGE SEGMENTELE DIN FOLDER (nu mai sunt copii, sunt originalele mutate) | |
| deleted_count = 0 | |
| total_deleted_size = 0 | |
| for file_to_delete in moved_files: | |
| try: | |
| file_size = os.path.getsize(file_to_delete) | |
| os.remove(file_to_delete) | |
| deleted_count += 1 | |
| total_deleted_size += file_size | |
| print(f" 🗑️ Șters segment: {os.path.basename(file_to_delete)}") | |
| except Exception as e: | |
| print(f" ⚠ Nu am putut șterge {file_to_delete}: {e}") | |
| deleted_size_mb = total_deleted_size / (1024 * 1024) | |
| print(f"🎉 FINALIZAT: Păstrat doar fișierul combinat '{os.path.basename(output_file)}'") | |
| print(f"🗑️ Șterse {deleted_count} segmente originale ({deleted_size_mb:.2f} MB)") | |
| print(f"💾 BACKUP SIGUR: Segmentele originale păstrate în {backup_dir}") | |
| else: | |
| print(f"❌ EROARE: Fișierul combinat nu a fost creat corect!") | |
| print(f"🛡️ SIGURANȚĂ: Păstrez segmentele pentru siguranță") | |
| print(f"💾 BACKUP DISPONIBIL: {backup_dir}") | |
| elif len(moved_files) == 1: | |
| # Un singur fișier - doar redenumește | |
| original_file = moved_files[0] | |
| original_size_mb = os.path.getsize(original_file) / (1024 * 1024) | |
| try: | |
| os.replace(original_file, output_file) | |
| print(f"✅ Fișierul redenumit în: {os.path.basename(output_file)} ({original_size_mb:.2f} MB)") | |
| print(f"💾 BACKUP SIGUR: Originalul păstrat în {backup_dir}") | |
| except Exception as e: | |
| print(f"⚠ Nu am putut redenumi {original_file}: {e}") | |
| else: | |
| print(f"ℹ️ Nu există fișiere PDF de combinat în '{dest_dir}'.") | |
| except Exception as e: | |
| print(f"❌ EROARE la combinarea PDF-urilor: {e}") | |
| print(f"🛡️ SIGURANȚĂ: Păstrez segmentele din cauza erorii") | |
| print(f"💾 BACKUP DISPONIBIL: {backup_dir}") | |
| return | |
| # PASUL 4: Raport final | |
| try: | |
| if os.path.exists(output_file): | |
| final_size_mb = os.path.getsize(output_file) / (1024 * 1024) | |
| print(f"\n📋 RAPORT FINAL pentru '{issue_title}':") | |
| print(f" 📁 Folder destinație: {dest_dir}") | |
| print(f" 📄 Fișier final: {os.path.basename(output_file)} ({final_size_mb:.2f} MB)") | |
| print(f" 🔍 Combinat din {len(all_segments)} segmente") | |
| print(f" 💾 BACKUP SIGUR: {backup_dir} ({backup_size_mb:.2f} MB)") | |
| print(f" ✅ STATUS: SUCCES - fișier complet creat, backup realizat, segmente șterse de pe G:\\") | |
| else: | |
| print(f"⚠ Nu s-a putut crea fișierul final pentru '{issue_title}'") | |
| print(f"💾 BACKUP DISPONIBIL: {backup_dir}") | |
| except Exception as e: | |
| print(f"⚠ Eroare la raportul final: {e}") | |
| print(f"=" * 60) | |
| def find_next_issue_in_collection_order(self, collection_links, last_completed_url): | |
| """ | |
| FIXED: Găsește următorul issue de procesat în ordinea din HTML, nu primul din listă | |
| """ | |
| if not last_completed_url: | |
| # Dacă nu avem istoric, începe cu primul din listă | |
| return collection_links[0] if collection_links else None | |
| try: | |
| last_index = collection_links.index(last_completed_url.rstrip('/')) | |
| # Returnează următorul din listă după cel completat | |
| next_index = last_index + 1 | |
| if next_index < len(collection_links): | |
| next_url = collection_links[next_index] | |
| print(f"🎯 Următorul issue după '{last_completed_url}' este: '{next_url}'") | |
| return next_url | |
| else: | |
| print(f"✅ Toate issue-urile din colecție au fost procesate!") | |
| return None | |
| except ValueError: | |
| # Dacă last_completed_url nu e în lista curentă, începe cu primul | |
| print(f"⚠ URL-ul '{last_completed_url}' nu e în colecția curentă, încep cu primul din listă") | |
| return collection_links[0] if collection_links else None | |
| def get_last_completed_issue_from_collection(self, collection_links): | |
| """FIXED: Găsește ultimul issue REAL complet descărcat din colecția curentă""" | |
| for item in self.state.get("downloaded_issues", []): | |
| url = item.get("url", "").rstrip('/') | |
| if url in [link.rstrip('/') for link in collection_links]: | |
| # VERIFICARE CORECTĂ: Issue-ul trebuie să fie REAL complet | |
| if self.is_issue_really_complete(item): | |
| print(f"🏁 Ultimul issue REAL complet din colecție: {url}") | |
| return url | |
| elif item.get("completed_at"): | |
| last_segment = item.get("last_successful_segment_end", 0) | |
| total_pages = item.get("total_pages") | |
| pages = item.get("pages", 0) | |
| print(f"⚠ Issue marcat ca complet dar INCOMPLET: {url} ({last_segment}/{total_pages}, pages: {pages})") | |
| print("🆕 Niciun issue REAL complet găsit în colecția curentă") | |
| return None | |
| def open_new_tab_and_download(self, url): | |
| """FIXED: Verificare simplă din JSON - fără verificare fizică""" | |
| normalized_url = url.rstrip('/') | |
| # Skip URLs din lista statică | |
| if normalized_url in self.dynamic_skip_urls: | |
| print(f"⏭️ Sar peste {url} (în skip list).") | |
| return False | |
| # ✅ VERIFICARE SIMPLIFICATĂ - doar din JSON | |
| # Dacă are completed_at ȘI pages > 0, e complet și procesat! | |
| already_done = any( | |
| item.get("url") == normalized_url and | |
| item.get("completed_at") and | |
| item.get("pages", 0) > 0 and | |
| item.get("total_pages") and | |
| item.get("pages") == item.get("total_pages") | |
| for item in self.state.get("downloaded_issues", []) | |
| ) | |
| if already_done: | |
| print(f"⏭️ Sar peste {url} (deja descărcat și procesat complet în JSON).") | |
| return False | |
| print(f"\n🎯 ÎNCEP FOCUSAREA PE: {url}") | |
| print("=" * 60) | |
| try: | |
| if not self.attached_existing: | |
| self.ensure_alive_fallback() | |
| # Deschide fila nouă | |
| prev_handles = set(self.driver.window_handles) | |
| self.driver.execute_script("window.open('');") | |
| new_handles = set(self.driver.window_handles) | |
| diff = new_handles - prev_handles | |
| new_handle = diff.pop() if diff else self.driver.window_handles[-1] | |
| self.driver.switch_to.window(new_handle) | |
| if not self.navigate_to_page(url): | |
| print(f"❌ Nu am putut naviga la {url}") | |
| return False | |
| time.sleep(2) | |
| # VERIFICARE NOUĂ: Este nevoie de login? | |
| if self.detect_login_required(): | |
| print("🔐 DETECTAT: Este necesară autentificarea!") | |
| print("🔄 Încerc login automat...") | |
| login_success = self.perform_auto_login() | |
| if not login_success: | |
| print("❌ LOGIN EȘUAT - Opresc procesarea acestui issue") | |
| return False | |
| print("✅ Login reușit - renavigez la issue...") | |
| # După login, renavigăm la issue-ul original | |
| if not self.navigate_to_page(url): | |
| print(f"❌ Nu pot renaviga la {url} după login") | |
| return False | |
| time.sleep(3) | |
| print("✅ Renavigat cu succes după login") | |
| # Verifică DOAR o dată la început pentru limită | |
| if self.check_daily_limit_in_all_windows(set_flag=False): | |
| print("⚠ Pagină cu limită zilnică detectată - opresc aici.") | |
| self.state["daily_limit_hit"] = True | |
| self._save_state() | |
| return False | |
| print("✅ Pagina e OK, încep extragerea metadatelor...") | |
| title, subtitle = self.get_issue_metadata() | |
| # FIXED: Scanează corect fișierele existente pentru acest issue specific | |
| existing_pages = self.get_existing_pdf_segments(url) | |
| print(f"📊 Pagini existente pe disk: {existing_pages}") | |
| resume_from = 1 | |
| json_progress = 0 | |
| for item in self.state.get("downloaded_issues", []): | |
| if item.get("url") == normalized_url: | |
| json_progress = item.get("last_successful_segment_end", 0) | |
| total_pages = item.get("total_pages") | |
| if json_progress and total_pages and json_progress >= total_pages: | |
| resume_from = None | |
| break | |
| actual_progress = max(json_progress, existing_pages) | |
| if actual_progress > 0 and resume_from is not None: | |
| resume_from = actual_progress + 1 | |
| print(f"📄 Reiau de la pagina {resume_from} (JSON: {json_progress}, Disk: {existing_pages})") | |
| if resume_from is None: | |
| print(f"⏭️ Issue-ul {url} este deja complet.") | |
| return False | |
| self.current_issue_url = normalized_url | |
| # FIXED: Obține total_pages și actualizează progresul | |
| total_pages = self.get_total_pages() | |
| if total_pages > 0: | |
| self._update_partial_issue_progress(normalized_url, actual_progress, total_pages=total_pages, title=title, subtitle=subtitle) | |
| else: | |
| print("⚠ Nu am putut obține numărul total de pagini!") | |
| print(f"🔒 INTRÂND ÎN MODUL FOCUS - nu mai fac alte verificări până nu termin!") | |
| # ==================== DESCĂRCAREA PROPRIU-ZISĂ ==================== | |
| print(f"📥 ÎNCEPE DESCĂRCAREA pentru {url}...") | |
| pages_done, limit_hit = self.save_all_pages_in_batches(resume_from=resume_from) | |
| print(f"📊 REZULTAT DESCĂRCARE:") | |
| print(f" 📄 Pagini descărcate: {pages_done}") | |
| print(f" 📄 Total necesar: {total_pages}") | |
| print(f" 🛑 Limită zilnică: {limit_hit}") | |
| if pages_done == 0 and not limit_hit: | |
| print(f"⚠ Descărcarea pentru {url} a eșuat complet.") | |
| return False | |
| # ==================== VERIFICARE SEGMENTE LIPSĂ ==================== | |
| # 🔥 VERIFICARE OBLIGATORIE - chiar dacă a fost limită/CAPTCHA | |
| if total_pages > 0: | |
| print(f"\n🔍 VERIFICARE COMPLETITUDINE: Scanez după segmente lipsă...") | |
| print(f" (Această verificare se face ÎNTOTDEAUNA, indiferent de limită/CAPTCHA)") | |
| all_present, missing_segments = self.verify_all_segments_present(normalized_url, total_pages) | |
| if not all_present: | |
| print(f"🚨 GĂURI DETECTATE: {len(missing_segments)} segmente lipsă!") | |
| for start, end in missing_segments: | |
| print(f" 📄 LIPSEȘTE: pages{start}-{end}") | |
| # Dacă a fost limită zilnică, NU încerca să recuperezi acum | |
| if limit_hit: | |
| print(f"⚠️ LIMITĂ ZILNICĂ atinsă - nu pot recupera segmentele lipsă ACUM") | |
| print(f"🔄 Segmentele lipsă vor fi re-descărcate la următoarea rulare") | |
| print(f"🛡️ BLOCHEZ marcarea ca terminat - issue rămâne PARȚIAL") | |
| # Actualizează progresul ca parțial | |
| self._update_partial_issue_progress( | |
| normalized_url, pages_done, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| return False | |
| print(f"🔄 RECUPERARE AUTOMATĂ: Descarc segmentele lipsă...") | |
| recovery_success = self.download_missing_segments(normalized_url, missing_segments) | |
| if recovery_success: | |
| print(f"✅ RECUPERARE REUȘITĂ!") | |
| # Re-verifică | |
| all_present_2, missing_2 = self.verify_all_segments_present(normalized_url, total_pages) | |
| if not all_present_2: | |
| print(f"❌ Încă lipsesc {len(missing_2)} segmente după recuperare!") | |
| for start, end in missing_2[:5]: # Afișează primele 5 | |
| print(f" 📄 LIPSEȘTE: pages{start}-{end}") | |
| print(f"🛡️ BLOCHEZ marcarea ca terminat") | |
| # Marchează ca parțial pentru reluare | |
| self._update_partial_issue_progress( | |
| normalized_url, pages_done, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| return False | |
| # 🔥 CRITICAL FIX: Actualizează pages_done cu progresul REAL de pe disk după recuperare! | |
| print(f"🔄 ACTUALIZARE: Scanez disk-ul pentru progres REAL după recuperare...") | |
| final_segments_after_recovery = self.get_all_pdf_segments_for_issue(normalized_url) | |
| if final_segments_after_recovery: | |
| real_progress_after_recovery = max(seg['end'] for seg in final_segments_after_recovery) | |
| print(f"📊 Progres REAL după recuperare: {real_progress_after_recovery}/{total_pages}") | |
| # 🔥 ACTUALIZEAZĂ pages_done cu valoarea REALĂ! | |
| pages_done = real_progress_after_recovery | |
| print(f"✅ pages_done actualizat: {pages_done} pagini") | |
| else: | |
| print(f"⚠️ Nu am găsit segmente pe disk după recuperare!") | |
| return False | |
| else: | |
| print(f"❌ RECUPERARE EȘUATĂ") | |
| print(f"🛡️ BLOCHEZ marcarea ca terminat") | |
| # Marchează ca parțial pentru reluare | |
| self._update_partial_issue_progress( | |
| normalized_url, pages_done, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| return False | |
| else: | |
| print(f"✅ TOATE segmentele sunt prezente - nicio gaură!") | |
| if limit_hit: | |
| print(f"⚠ Limită zilnică atinsă în timpul descărcării pentru {url}; progresul parțial a fost salvat.") | |
| return False | |
| # ==================== VERIFICĂRI ULTRA SAFE ÎNAINTE DE FINALIZARE ==================== | |
| print(f"🔍 VERIFICĂRI FINALE ULTRA SAFE pentru {url}...") | |
| print(f"📊 Rezultat descărcare: {pages_done} pagini din {total_pages}") | |
| # Verifică dacă total_pages a fost detectat corect | |
| if total_pages <= 0: | |
| print(f"❌ OPRIRE SAFETY: total_pages nu a fost detectat corect ({total_pages})") | |
| print(f"🛡️ NU marchez ca terminat fără total_pages valid") | |
| print(f"🔄 Păstrez ca parțial cu progres {pages_done}") | |
| self._update_partial_issue_progress( | |
| normalized_url, pages_done, total_pages=None, title=title, subtitle=subtitle | |
| ) | |
| return True # Succes parțial | |
| # VERIFICARE CRITICĂ: Progresul trebuie să fie aproape complet | |
| completion_percent = (pages_done / total_pages) * 100 | |
| print(f"📊 Completitudine calculată: {completion_percent:.1f}%") | |
| if completion_percent < 95: # Cel puțin 95% | |
| print(f"❌ BLOCARE SAFETY: Progres insuficient pentru marcare ca terminat") | |
| print(f"📊 Progres: {pages_done}/{total_pages} ({completion_percent:.1f}%)") | |
| print(f"🛡️ Trebuie cel puțin 95% pentru a marca ca terminat!") | |
| print(f"🔄 Păstrez ca PARȚIAL pentru continuare ulterioară") | |
| # Marchează ca parțial, NU ca terminat | |
| self._update_partial_issue_progress( | |
| normalized_url, pages_done, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| print(f"💾 Issue {url} păstrat ca parțial: {pages_done}/{total_pages}") | |
| print(f"🔄 Va fi continuat automat la următoarea rulare") | |
| return True # Succes parțial - va continua mai târziu | |
| # VERIFICARE SUPLIMENTARĂ: Issues mari cu progres mic | |
| if total_pages >= 500 and pages_done < 200: | |
| print(f"❌ BLOCARE SPECIALĂ: Issue mare cu progres suspect de mic") | |
| print(f"📊 {pages_done} pagini din {total_pages} pare eșec de descărcare!") | |
| print(f"🛡️ Probabil eroare tehnică sau limită - NU marchez terminat") | |
| self._update_partial_issue_progress( | |
| normalized_url, pages_done, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| return True # Succes parțial | |
| # ===== VERIFICARE ULTRA-CRITICĂ FINALĂ: SCANEAZĂ DISK-UL ÎNAINTE DE MARCARE ===== | |
| print(f"\n🔍 VERIFICARE ULTRA-CRITICĂ FINALĂ: Scanez disk-ul pentru progres EFECTIV ÎNAINTE de marcare...") | |
| print("⏳ SINCRONIZARE: Aștept 30 secunde ca toate fișierele să fie complet salvate pe disk...") | |
| time.sleep(30) | |
| final_segments_check = self.get_all_pdf_segments_for_issue(url) | |
| if not final_segments_check: | |
| print(f"❌ PROBLEMĂ GRAVĂ: Nu am găsit NICIUN segment pe disk!") | |
| print(f"🛡️ BLOCHEZ marcarea ca terminat - ceva s-a întâmplat grav!") | |
| return False | |
| # Calculează progresul REAL de pe disk | |
| real_final_page = max(seg['end'] for seg in final_segments_check) | |
| print(f"📊 PROGRES REAL DE PE DISK: {real_final_page}/{total_pages}") | |
| print(f"📄 Segmente găsite: {len(final_segments_check)}") | |
| # Verifică dacă progresul REAL este suficient | |
| real_completion_percent = (real_final_page / total_pages) * 100 if total_pages > 0 else 0 | |
| if real_completion_percent < 95: | |
| print(f"❌ BLOCARE SAFETY: Progresul REAL de pe disk este prea mic!") | |
| print(f"📊 Progres REAL: {real_final_page}/{total_pages} ({real_completion_percent:.1f}%)") | |
| print(f"🛡️ pages_done din descărcare zicea {pages_done}, dar disk-ul arată {real_final_page}") | |
| print(f"🔄 Marchează ca PARȚIAL pentru continuare ulterioară") | |
| # Actualizează cu progresul REAL | |
| self._update_partial_issue_progress( | |
| normalized_url, real_final_page, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| print(f"💾 Issue {url} păstrat ca parțial cu progres REAL: {real_final_page}/{total_pages}") | |
| return True # Succes parțial | |
| # Identifică și afișează găurile | |
| all_segments_expected = [] | |
| bs = self.batch_size | |
| first_end = min(bs - 1, total_pages) | |
| if first_end >= 1: | |
| all_segments_expected.append((1, first_end)) | |
| current = bs | |
| while current <= total_pages: | |
| end = min(current + bs - 1, total_pages) | |
| all_segments_expected.append((current, end)) | |
| current += bs | |
| missing_segments_final = [] | |
| for expected_start, expected_end in all_segments_expected: | |
| found = False | |
| for disk_seg in final_segments_check: | |
| if disk_seg['start'] <= expected_start and disk_seg['end'] >= expected_end: | |
| found = True | |
| break | |
| if not found: | |
| missing_segments_final.append((expected_start, expected_end)) | |
| if missing_segments_final: | |
| print(f"⚠️ GĂURI DETECTATE în verificarea finală: {len(missing_segments_final)} segmente!") | |
| for start, end in missing_segments_final[:5]: | |
| print(f" ❌ LIPSEȘTE: pages{start}-{end}") | |
| if len(missing_segments_final) > 5: | |
| print(f" ... și încă {len(missing_segments_final) - 5} segmente") | |
| print(f"🛡️ BLOCHEZ marcarea ca terminat din cauza găurilor!") | |
| # Marchează ca parțial cu progresul real | |
| self._update_partial_issue_progress( | |
| normalized_url, real_final_page, total_pages=total_pages, title=title, subtitle=subtitle | |
| ) | |
| return True # Succes parțial | |
| # ACTUALIZEAZĂ pages_done cu valoarea REALĂ de pe disk | |
| pages_done = real_final_page | |
| print(f"✅ VERIFICARE OK: Toate segmentele sunt prezente pe disk!") | |
| print(f"📊 Progres CONFIRMAT: {pages_done}/{total_pages} ({real_completion_percent:.1f}%)") | |
| # ===== TOATE VERIFICĂRILE AU TRECUT - MARCHEAZĂ CA TERMINAT ===== | |
| print(f"\n✅ TOATE VERIFICĂRILE AU TRECUT pentru {url}") | |
| print(f"🎯 Progres verificat pe disk: {pages_done}/{total_pages} ({real_completion_percent:.1f}%)") | |
| print(f"🎯 MARCHEZ CA TERMINAT COMPLET în JSON") | |
| # MARCHEAZĂ ISSUE CA TERMINAT | |
| self.mark_issue_done(url, pages_done, title=title, subtitle=subtitle, total_pages=total_pages) | |
| print(f"✅ Issue marcat ca terminat în JSON: {url} ({pages_done} pagini)") | |
| # PAUZĂ: Așteaptă ca JSON să fie salvat | |
| print("⏳ SINCRONIZARE: Aștept 3 secunde pentru salvarea JSON...") | |
| time.sleep(5) | |
| # ==================== PROCESAREA PDF-URILOR ==================== | |
| print(f"\n🔄 ÎNCEPE PROCESAREA PDF-URILOR pentru {url}...") | |
| # Verifică din nou că toate fișierele sunt pe disk | |
| final_segments = self.get_all_pdf_segments_for_issue(url) | |
| print(f"🔍 VERIFICARE: Am găsit {len(final_segments)} fișiere PDF pentru acest issue") | |
| if len(final_segments) == 0: | |
| print(f"⚠ PROBLEMĂ: Nu am găsit fișiere PDF pentru {url}!") | |
| return False | |
| # Copiază și combină PDF-urile | |
| print(f"📦 Copiez și combin toate PDF-urile...") | |
| self.copy_and_combine_issue_pdfs(url, title or normalized_url) | |
| print(f"✅ PDF-urile au fost copiate și combinate cu succes!") | |
| # PAUZĂ CRITICĂ 3: Așteaptă ca procesarea PDF să fie completă | |
| print("⏳ SINCRONIZARE: Aștept 15 secunde după procesarea PDF-urilor...") | |
| time.sleep(15) | |
| # ==================== FINALIZARE COMPLETĂ ==================== | |
| print("=" * 60) | |
| print(f"🎉 FOCUSAREA COMPLETĂ PE {url} FINALIZATĂ CU SUCCES!") | |
| print(f"📊 REZULTAT: {pages_done} pagini descărcate și procesate") | |
| print("=" * 60) | |
| # PAUZĂ FINALĂ: Înainte să treacă la următorul issue | |
| print("⏳ PAUZĂ FINALĂ: 10 secunde înainte de următorul issue...") | |
| time.sleep(10) | |
| return True | |
| except WebDriverException as e: | |
| print(f"❌ Eroare WebDriver pentru {url}: {e}") | |
| return False | |
| except Exception as e: | |
| print(f"❌ Eroare în open_new_tab_and_download pentru {url}: {e}") | |
| return False | |
| finally: | |
| try: | |
| # NU ÎNCHIDE DACĂ E ULTIMA FEREASTRĂ | |
| if len(self.driver.window_handles) > 1: | |
| self.driver.close() | |
| self.driver.switch_to.window(self.driver.window_handles[0]) | |
| else: | |
| # Doar revine la prima fereastră fără să închidă | |
| if self.driver.window_handles: | |
| self.driver.switch_to.window(self.driver.window_handles[0]) | |
| except Exception as e: | |
| print(f"⚠ Eroare în finally: {e}") | |
| pass | |
| def ensure_alive_fallback(self): | |
| try: | |
| _ = self.driver.title | |
| except Exception as e: | |
| print(f"⚠ Conexiune WebDriver moartă ({e}), pornesc instanță nouă ca fallback.") | |
| prefs = { | |
| "download.default_directory": os.path.abspath(self.download_dir), | |
| "download.prompt_for_download": False, | |
| "download.directory_upgrade": True, | |
| "safebrowsing.enabled": True, | |
| } | |
| chrome_options = Options() | |
| chrome_options.add_experimental_option("prefs", prefs) | |
| chrome_options.add_argument("--no-sandbox") | |
| chrome_options.add_argument("--disable-dev-shm-usage") | |
| chrome_options.add_argument("--disable-gpu") | |
| chrome_options.add_argument("--window-size=1920,1080") | |
| chrome_options.add_argument("--incognito") | |
| self.driver = webdriver.Chrome(options=chrome_options) | |
| self.wait = WebDriverWait(self.driver, self.timeout) | |
| self.attached_existing = False | |
| def run_collection(self, collection_url): | |
| """FIXED: Verifică TOATE issue-urile înainte să marcheze colecția ca completă""" | |
| print(f"🌐 Încep procesarea colecției: {collection_url}") | |
| if not self.driver: | |
| print("❌ Driver neinițializat.") | |
| return False | |
| if not self.navigate_to_page(collection_url): | |
| return False | |
| # Verifică limita DOAR la început | |
| if self.state.get("daily_limit_hit", False): | |
| print("⚠ Nu mai pot continua din cauza limitei zilnice setate anterior.") | |
| return False # SCHIMBAT din True în False | |
| if self.remaining_quota() <= 0: | |
| print(f"⚠ Limita zilnică de {DAILY_LIMIT} issue-uri atinsă.") | |
| return False # SCHIMBAT din True în False | |
| issue_links = self.extract_issue_links_from_collection() | |
| if not issue_links: | |
| print("⚠ Nu s-au găsit issue-uri în colecție.") | |
| return False | |
| # === VERIFICARE CRITICĂ: Scanează TOATE issue-urile din colecție (SILENȚIOS pentru skip) === | |
| print(f"\n🔍 VERIFICARE COMPLETITUDINE COLECȚIE:") | |
| incomplete_issues = [] | |
| complete_count = 0 | |
| skipped_count = 0 | |
| for link in issue_links: | |
| normalized = link.rstrip('/') | |
| # SKIP SILENȚIOS dacă e în skip list | |
| if normalized in self.dynamic_skip_urls: | |
| skipped_count += 1 | |
| continue | |
| # Verifică în state.json | |
| issue_item = next( | |
| (i for i in self.state.get("downloaded_issues", []) | |
| if i.get("url") == normalized), | |
| None | |
| ) | |
| if not issue_item: | |
| # Issue nou - trebuie descărcat | |
| incomplete_issues.append(link) | |
| print(f" 🆕 NOU: {link}") | |
| continue | |
| # Verifică dacă e REAL complet | |
| is_really_complete = self.is_issue_really_complete(issue_item) | |
| if is_really_complete: | |
| complete_count += 1 | |
| # NU mai afișa pentru fiecare - doar contorizează | |
| else: | |
| incomplete_issues.append(link) | |
| last_segment = issue_item.get("last_successful_segment_end", 0) | |
| total_pages = issue_item.get("total_pages", "?") | |
| pages = issue_item.get("pages", 0) | |
| print(f" ❌ INCOMPLET: {link}") | |
| print(f" pages: {pages}, last_segment: {last_segment}, total: {total_pages}") | |
| # === RAPORT COLECȚIE (DUPĂ SCANAREA TUTUROR ISSUE-URILOR) === | |
| print(f"\n📊 RAPORT COLECȚIE:") | |
| print(f" Total issues: {len(issue_links)}") | |
| print(f" ⏭️ Skip (în skip_urls.json): {skipped_count}") | |
| print(f" ✅ Complete: {complete_count}") | |
| print(f" ❌ Incomplete: {len(incomplete_issues)}") | |
| # === DACĂ NU E NIMIC DE PROCESAT, COLECȚIA E COMPLETĂ === | |
| if len(incomplete_issues) == 0: | |
| print(f"✅ COLECȚIA {collection_url} ESTE COMPLETĂ!") | |
| print(f" Toate {len(issue_links)} issue-uri sunt complete") | |
| return True | |
| # === ÎNAINTE DE A PROCESA ISSUE-URI INCOMPLETE: Finalizează issue-uri complet descărcate dar nefinalizate === | |
| print(f"\n🔍 VERIFICARE PRIORITARĂ: Caut issue-uri complet descărcate dar nefinalizate din această colecție...") | |
| # Apelează procesarea issue-urilor nefinalizate pentru această colecție | |
| self.process_completed_but_unfinalized_issues() | |
| print(f"✅ Verificare completă - continuez cu issue-urile incomplete") | |
| # === PROCESEAZĂ ISSUE-URILE INCOMPLETE === | |
| print(f"\n🎯 PROCESEZ {len(incomplete_issues)} issue-uri incomplete:") | |
| processed_any = False | |
| for i, link in enumerate(incomplete_issues): | |
| print(f"\n🔢 ISSUE {i+1}/{len(incomplete_issues)}: {link}") | |
| # Verifică cota | |
| if self.remaining_quota() <= 0: | |
| print(f"⚠ Limită zilnică atinsă - opresc procesarea") | |
| return False # Returnează FALSE ca să nu marcheze colecția ca completă | |
| if self.state.get("daily_limit_hit", False): | |
| print("⚠ Flag daily_limit_hit setat - opresc procesarea") | |
| return False | |
| # Procesează issue-ul | |
| result = self.open_new_tab_and_download(link) | |
| if result: | |
| processed_any = True | |
| print(f"✅ Issue-ul {link} procesat cu succes!") | |
| else: | |
| print(f"⚠ Issue-ul {link} nu a fost procesat") | |
| # Verifică din nou cota | |
| if self.remaining_quota() <= 0 or self.state.get("daily_limit_hit", False): | |
| print("⚠ Limită zilnică atinsă - opresc procesarea") | |
| return False | |
| # Pauză între issue-uri | |
| if i < len(incomplete_issues) - 1: | |
| print("⏳ Pauză de 2s între issue-uri...") | |
| time.sleep(2) | |
| # === RE-VERIFICARE FINALĂ === | |
| print(f"\n🔍 RE-VERIFICARE FINALĂ după procesare:") | |
| still_incomplete = [] | |
| for link in issue_links: | |
| normalized = link.rstrip('/') | |
| issue_item = next( | |
| (i for i in self.state.get("downloaded_issues", []) | |
| if i.get("url") == normalized), | |
| None | |
| ) | |
| if not issue_item or not self.is_issue_really_complete(issue_item): | |
| still_incomplete.append(link) | |
| if len(still_incomplete) == 0: | |
| print(f"✅ COLECȚIA {collection_url} ESTE ACUM COMPLETĂ!") | |
| return True | |
| else: | |
| print(f"⚠ COLECȚIA {collection_url} ÎNCĂ ARE {len(still_incomplete)} issue-uri incomplete") | |
| print(f" Va fi reluată la următoarea rulare") | |
| return False | |
| def process_pending_partials_first(self): | |
| """FIXED: Procesează mai întâi issue-urile parțiale, indiferent de colecție""" | |
| pending_partials = self.get_pending_partial_issues() | |
| if not pending_partials: | |
| print("✅ Nu există issue-uri parțiale de procesat.") | |
| return True | |
| print(f"\n🎯 PRIORITATE: Procesez {len(pending_partials)} issue-uri parțiale:") | |
| for item in pending_partials: | |
| url = item.get("url") | |
| progress = item.get("last_successful_segment_end", 0) | |
| total = item.get("total_pages", 0) | |
| print(f" 🔄 {url} - pagini {progress}/{total}") | |
| # Procesează issue-urile parțiale | |
| processed_any = False | |
| for item in pending_partials: | |
| if self.remaining_quota() <= 0 or self.state.get("daily_limit_hit", False): | |
| print(f"⚠ Limita zilnică atinsă în timpul issue-urilor parțiale.") | |
| break | |
| url = item.get("url") | |
| result = self.open_new_tab_and_download(url) | |
| if result: | |
| processed_any = True | |
| time.sleep(1) | |
| return processed_any | |
| def run_additional_collections(self): | |
| """FIXED: Nu sare la următoarea colecție dacă cea curentă nu e completă""" | |
| start_index = self.state.get("current_additional_collection_index", 0) | |
| if start_index >= len(ADDITIONAL_COLLECTIONS): | |
| print("✅ TOATE colecțiile adiționale au fost procesate!") | |
| return True | |
| print(f"🔄 Continuez cu colecțiile adiționale de la indexul {start_index}") | |
| for i in range(start_index, len(ADDITIONAL_COLLECTIONS)): | |
| collection_url = ADDITIONAL_COLLECTIONS[i] | |
| print(f"\n📚 COLECȚIA {i+1}/{len(ADDITIONAL_COLLECTIONS)}: {collection_url}") | |
| if collection_url.rstrip('/') in self.dynamic_skip_urls: | |
| print(f"⏭️ Skip colecția (deja completă)") | |
| self.state["current_additional_collection_index"] = i + 1 | |
| self._save_state() | |
| continue | |
| if self.remaining_quota() <= 0 or self.state.get("daily_limit_hit", False): | |
| print(f"⚠ Limită zilnică - opresc procesarea") | |
| return False # SCHIMBAT | |
| # Setează indexul ÎNAINTE de procesare | |
| self.state["current_additional_collection_index"] = i | |
| self._save_state() | |
| # Procesează colecția | |
| collection_completed = self.run_collection(collection_url) | |
| if collection_completed: | |
| # DOAR dacă e REAL completă | |
| print(f"✅ Colecția {i+1} COMPLETĂ - trec la următoarea") | |
| self.mark_collection_complete(collection_url) | |
| self.state["current_additional_collection_index"] = i + 1 | |
| self._save_state() | |
| else: | |
| # NU e completă - oprește aici | |
| print(f"⚠ Colecția {i+1} NU e completă - rămân aici") | |
| print(f"🔄 Va continua cu aceeași colecție la următoarea rulare") | |
| return False # OPREȘTE - nu trece la următoarea | |
| if self.state.get("daily_limit_hit", False): | |
| return False | |
| print("🎉 TOATE colecțiile au fost procesate!") | |
| return True | |
| def run(self): | |
| print("🧪 Încep executarea Chrome PDF Downloader FIXED") | |
| print("=" * 60) | |
| try: | |
| if not self.setup_chrome_driver(): | |
| return False | |
| # 🚨 VERIFICARE CAPTCHA DIN RULAREA ANTERIOARĂ | |
| if self.state.get("captcha_detected", False): | |
| print(f"\n{'='*60}") | |
| print(f"🚨 CAPTCHA DETECTAT ÎN RULAREA ANTERIOARĂ!") | |
| print(f"{'='*60}") | |
| print(f"❌ Scriptul a fost oprit anterior din cauza CAPTCHA") | |
| print(f"📋 URL CAPTCHA: {self.state.get('captcha_url', 'necunoscut')}") | |
| print(f"⚠️ ACȚIUNE NECESARĂ:") | |
| print(f" 1. Rezolvă CAPTCHA manual în browser") | |
| print(f" 2. Șterge flag-ul din state.json:") | |
| print(f" \"captcha_detected\": false") | |
| print(f" 3. Repornește scriptul") | |
| print(f"{'='*60}\n") | |
| return False | |
| print("🔄 Resetez flag-ul de limită zilnică...") | |
| self.state["daily_limit_hit"] = False | |
| self._save_state() | |
| # Sincronizare și cleanup | |
| self.sync_json_with_disk_files() | |
| # Procesează issues complet descărcate dar nefinalizate | |
| self.process_completed_but_unfinalized_issues() | |
| self.cleanup_duplicate_issues() | |
| self.fix_incorrectly_marked_complete_issues() | |
| # === NOUĂ VERIFICARE: Corectează progresul bazat pe disk === | |
| print("\n🔍 VERIFICARE: Sincronizez progresul din JSON cu fișierele de pe disk...") | |
| self.fix_progress_based_on_disk() | |
| if self.check_daily_limit_in_all_windows(set_flag=False): | |
| print("⚠ Am găsit ferestre cu limita deschise - le închid...") | |
| # === BUCLĂ PRINCIPALĂ: Verifică MEREU issue-uri parțiale === | |
| max_iterations = 100 # Prevenire buclă infinită | |
| iteration = 0 | |
| while iteration < max_iterations: | |
| iteration += 1 | |
| print(f"\n{'='*60}") | |
| print(f"🔄 ITERAȚIE {iteration}: Verificare priorități") | |
| print(f"{'='*60}") | |
| # === PRIORITATE 0: Issue-uri complet descărcate dar nefinalizate (PRIORITATE MAXIMĂ!) === | |
| print(f"\n🔍 PRIORITATE 0: Verific issue-uri complet descărcate dar nefinalizate...") | |
| self.process_completed_but_unfinalized_issues() | |
| print(f"✅ Verificare issue-uri nefinalizate completă") | |
| # === PRIORITATE 1: Issue-uri parțiale (din ORICE colecție) === | |
| pending_partials = self.get_pending_partial_issues() | |
| if pending_partials: | |
| print(f"\n🎯 PRIORITATE ABSOLUTĂ: {len(pending_partials)} issue-uri parțiale găsite") | |
| for idx, item in enumerate(pending_partials): | |
| if self.remaining_quota() <= 0 or self.state.get("daily_limit_hit", False): | |
| print("⚠ Limită zilnică atinsă") | |
| return True | |
| url = item.get("url") | |
| progress = item.get("last_successful_segment_end", 0) | |
| total = item.get("total_pages", 0) | |
| print(f"\n🔄 PARȚIAL {idx+1}/{len(pending_partials)}: {url}") | |
| print(f" Progres: {progress}/{total} pagini") | |
| result = self.open_new_tab_and_download(url) | |
| if result: | |
| print(f"✅ Issue parțial finalizat") | |
| else: | |
| print(f"⚠ Issue parțial nu s-a finalizat") | |
| time.sleep(2) | |
| # După ce procesezi parțiale, revino la început pentru re-verificare | |
| continue | |
| print("✅ Nu există issue-uri parțiale - continui cu colecțiile") | |
| # === PRIORITATE 2: Colecția principală === | |
| if not self.state.get("main_collection_completed", False): | |
| print(f"\n📚 Procesez colecția principală: {self.main_collection_url}") | |
| main_completed = self.run_collection(self.main_collection_url) | |
| if self.state.get("daily_limit_hit", False): | |
| print("⚠ Limită zilnică în colecția principală") | |
| return True | |
| if main_completed: | |
| print("✅ Colecția principală completă!") | |
| self.state["main_collection_completed"] = True | |
| self._save_state() | |
| # Revino la început pentru a verifica parțiale | |
| continue | |
| else: | |
| print("🔄 Colecția principală incompletă - reiau mai târziu") | |
| # Revino la început pentru a verifica parțiale | |
| continue | |
| print("✅ Colecția principală completă - trec la adiționale") | |
| # === PRIORITATE 3: Colecții adiționale === | |
| if self.remaining_quota() > 0 and not self.state.get("daily_limit_hit", False): | |
| print(f"\n📚 Procesez colecții adiționale") | |
| all_additional_complete = self.run_additional_collections() | |
| if all_additional_complete: | |
| print("🎉 TOATE colecțiile procesate!") | |
| return True | |
| else: | |
| print("🔄 Mai sunt colecții de procesat") | |
| # Revino la început pentru a verifica parțiale | |
| continue | |
| # Dacă ajungi aici, verifică o ultimă dată | |
| final_partials = self.get_pending_partial_issues() | |
| if not final_partials: | |
| print("✅ Nu mai există issue-uri parțiale - terminat!") | |
| break | |
| if iteration >= max_iterations: | |
| print("⚠ Limită iterații atinsă - posibilă buclă infinită") | |
| print("✅ Toate operațiunile finalizate") | |
| self._finalize_session() | |
| return True | |
| except KeyboardInterrupt: | |
| print("\n\n⚠ Intervenție manuală") | |
| return False | |
| except Exception as e: | |
| print(f"\n❌ Eroare: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return False | |
| finally: | |
| if not self.attached_existing and self.driver: | |
| try: | |
| self.driver.quit() | |
| except Exception: | |
| pass | |
| def _finalize_session(self): | |
| if self.driver: | |
| if self.attached_existing: | |
| print("🔖 Am păstrat sesiunea Chrome existentă deschisă (nu fac quit).") | |
| else: | |
| print("🚪 Închid browserul.") | |
| try: | |
| self.driver.quit() | |
| except Exception: | |
| pass | |
| def main(): | |
| """ | |
| MAIN FUNCTION CORECTATĂ - FOCUSEAZĂ PE StudiiSiCercetariMecanicaSiAplicata | |
| Nu mai sare la alte colecții până nu termină cu aceasta complet! | |
| """ | |
| log_file = setup_logging() # ADĂUGAT - PRIMA LINIE | |
| print("🚀 PORNIRE SCRIPT - ANALIZA INIȚIALĂ") | |
| print("=" * 70) | |
| # PASUL 1: Creează downloader temporar pentru analiza stării | |
| temp_downloader = ChromePDFDownloader("temp", download_dir="G:\\", batch_size=50) | |
| # PASUL 2: Analizează starea curentă | |
| print("🔍 ANALIZA STĂRII CURENTE:") | |
| current_state = temp_downloader.state | |
| main_completed = current_state.get("main_collection_completed", False) | |
| current_index = current_state.get("current_additional_collection_index", 0) | |
| total_issues = len(current_state.get("downloaded_issues", [])) | |
| print(f" 📊 Total issues în state: {total_issues}") | |
| print(f" 🏁 Main collection completed: {main_completed}") | |
| print(f" 🔢 Current additional index: {current_index}") | |
| # PASUL 3: Verifică issue-urile parțiale (PRIORITATE ABSOLUTĂ) | |
| print(f"\n🎯 VERIFICARE ISSUE-URI PARȚIALE:") | |
| pending_partials = temp_downloader.get_pending_partial_issues() | |
| if pending_partials: | |
| print(f"🚨 GĂSITE {len(pending_partials)} ISSUE-URI PARȚIALE!") | |
| print(f"🔥 PRIORITATE ABSOLUTĂ - acestea trebuie continuate:") | |
| for item in pending_partials: | |
| url = item.get("url", "") | |
| progress = item.get("last_successful_segment_end", 0) | |
| total = item.get("total_pages", 0) | |
| title = item.get("title", "") | |
| print(f" 🔄 {title}") | |
| print(f" 📍 {url}") | |
| print(f" 🎯 CONTINUĂ de la pagina {progress + 1} (progres: {progress}/{total})") | |
| print(f"\n✅ VA PROCESA AUTOMAT issue-urile parțiale primul!") | |
| else: | |
| print(f"✅ Nu există issue-uri parțiale de procesat") | |
| # PASUL 4: Analizează progresul StudiiSiCercetariMecanicaSiAplicata | |
| print(f"\n📚 ANALIZA COLECȚIEI StudiiSiCercetariMecanicaSiAplicata:") | |
| # Lista completă a anilor disponibili din HTML (1954-1992, minus 1964) | |
| expected_years = [] | |
| for year in range(1954, 1993): # 1954-1992 | |
| if year != 1964: # 1964 nu există în colecție | |
| expected_years.append(year) | |
| # Verifică care ani au fost descărcați | |
| downloaded_years = [] | |
| partial_years = [] | |
| for item in current_state.get("downloaded_issues", []): | |
| url = item.get("url", "") | |
| if "StudiiSiCercetariMecanicaSiAplicata" in url: | |
| # Extrage anul din URL | |
| year_match = re.search(r'StudiiSiCercetariMecanicaSiAplicata_(\d{4})', url) | |
| if year_match: | |
| year = int(year_match.group(1)) | |
| if item.get("completed_at"): | |
| downloaded_years.append(year) | |
| else: | |
| partial_years.append(year) | |
| downloaded_years.sort() | |
| partial_years.sort() | |
| missing_years = [year for year in expected_years if year not in downloaded_years and year not in partial_years] | |
| print(f" 📅 Ani disponibili: {len(expected_years)} (1954-1992, minus 1964)") | |
| print(f" ✅ Ani descărcați: {len(downloaded_years)} - {downloaded_years}") | |
| print(f" 🔄 Ani parțiali: {len(partial_years)} - {partial_years}") | |
| print(f" ❌ Ani lipsă: {len(missing_years)} - {missing_years[:10]}{'...' if len(missing_years) > 10 else ''}") | |
| # PASUL 5: Determină strategia | |
| total_remaining = len(partial_years) + len(missing_years) | |
| if total_remaining > 0: | |
| print(f"\n🎯 STRATEGIA DE PROCESARE:") | |
| print(f" 🔥 RĂMÂN {total_remaining} ani de procesat din StudiiSiCercetariMecanicaSiAplicata") | |
| print(f" 🚫 NU se trece la alte colecții până nu se termină aceasta!") | |
| print(f" 📈 Progres: {len(downloaded_years)}/{len(expected_years)} ani completați ({len(downloaded_years)/len(expected_years)*100:.1f}%)") | |
| else: | |
| print(f"\n✅ StudiiSiCercetariMecanicaSiAplicata este COMPLET!") | |
| print(f" 🎯 Va trece la următoarea colecție din ADDITIONAL_COLLECTIONS") | |
| # PASUL 6: Resetează starea pentru a continua corect cu StudiiSiCercetariMecanicaSiAplicata | |
| if total_remaining > 0: | |
| print(f"\n🔧 RESETEZ STAREA pentru a continua cu StudiiSiCercetariMecanicaSiAplicata:") | |
| # Resetează flag-urile greșite | |
| if main_completed: | |
| print(f" 🔄 Resetez main_collection_completed: True → False") | |
| temp_downloader.state["main_collection_completed"] = False | |
| if current_index > 1: # StudiiSiCercetariMecanicaSiAplicata e pe index 1 | |
| print(f" 🔄 Resetez current_additional_collection_index: {current_index} → 1") | |
| temp_downloader.state["current_additional_collection_index"] = 1 | |
| temp_downloader._save_state() | |
| print(f" ✅ Starea resetată pentru a continua cu StudiiSiCercetariMecanicaSiAplicata") | |
| # PASUL 7: Setează URL-ul colecției principale (sare peste StudiiSiCercetariMecanicaSiAplicata) | |
| print(f"\n🎯 SELECTARE COLECȚIE PRINCIPALĂ:") | |
| # Găsește prima colecție din ADDITIONAL_COLLECTIONS care NU e în skip list | |
| main_collection_url = None | |
| for collection_url in ADDITIONAL_COLLECTIONS: | |
| normalized = collection_url.rstrip('/') | |
| if normalized not in temp_downloader.dynamic_skip_urls: | |
| main_collection_url = collection_url | |
| print(f"✅ SELECTAT: {collection_url}") | |
| break | |
| else: | |
| print(f"⏭️ SKIP (complet descărcat): {collection_url}") | |
| if not main_collection_url: | |
| print("❌ TOATE colecțiile au fost descărcate!") | |
| sys.exit(0) | |
| print(f"\n🚀 ÎNCEPE PROCESAREA:") | |
| print(f"📍 URL principal: {main_collection_url}") | |
| print(f"📁 Director descărcare: G:\\") | |
| print(f"📦 Batch size: 50 pagini per segment") | |
| if pending_partials: | |
| print(f"⚡ Va începe cu {len(pending_partials)} issue-uri parțiale") | |
| print("=" * 70) | |
| # PASUL 8: Creează downloader-ul principal și pornește procesarea | |
| try: | |
| downloader = ChromePDFDownloader( | |
| main_collection_url=main_collection_url, | |
| download_dir="G:\\", | |
| batch_size=50 | |
| ) | |
| print("🎯 ÎNCEPE EXECUȚIA PRINCIPALĂ...") | |
| success = downloader.run() | |
| if success: | |
| print("\n✅ EXECUȚIE FINALIZATĂ CU SUCCES!") | |
| else: | |
| print("\n⚠ EXECUȚIE FINALIZATĂ CU PROBLEME!") | |
| sys.exit(1) | |
| except KeyboardInterrupt: | |
| print("\n\n⚠ OPRIRE MANUALĂ - Progresul a fost salvat") | |
| sys.exit(0) | |
| except Exception as e: | |
| print(f"\n❌ EROARE FATALĂ în main(): {e}") | |
| import traceback | |
| traceback.print_exc() | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| try: | |
| main() | |
| except Exception as e: | |
| print(f"❌ Eroare fatală în __main__: {e}") | |
| sys.exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment