export_summary.txt wird nicht mehr erstellt
Dieser Commit ist enthalten in:
@ -6,7 +6,10 @@
|
||||
"Bash(git -C /mnt/a/GiTea/AccountForger log --oneline)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(sqlite3:*)",
|
||||
"Bash(find:*)"
|
||||
"Bash(find:*)",
|
||||
"Bash(pkill:*)",
|
||||
"Bash(lsof:*)",
|
||||
"Bash(ls:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": [],
|
||||
|
||||
@ -71,7 +71,10 @@ class PlaywrightManager:
|
||||
self.browser = None
|
||||
self.context = None
|
||||
self.page = None
|
||||
|
||||
|
||||
# Feature 5: Flag für Disconnect-Tracking (verhindert doppelte Counter-Dekrementierung)
|
||||
self._browser_was_disconnected = False
|
||||
|
||||
# Zähler für Wiederhholungsversuche
|
||||
self.retry_counter = {}
|
||||
|
||||
@ -141,12 +144,29 @@ class PlaywrightManager:
|
||||
if self.page is not None:
|
||||
return self.page
|
||||
|
||||
# Feature 5: Reset Disconnect-Flag bei neuem Browser-Start
|
||||
self._browser_was_disconnected = False
|
||||
|
||||
# Feature 5: Browser-Instanz Schutz - Nur eine Instanz gleichzeitig
|
||||
if PlaywrightManager._active_count >= 1:
|
||||
raise RuntimeError(
|
||||
"Browser bereits aktiv. Nur eine Browser-Instanz gleichzeitig erlaubt. "
|
||||
"Beenden Sie den aktuellen Prozess."
|
||||
)
|
||||
# Safety-Check: Prüfe ob Counter "hängt" (Absturz-Schutz)
|
||||
# Wenn ProcessGuard NICHT locked ist, aber Counter > 0, dann ist Counter "tot"
|
||||
from utils.process_guard import get_guard
|
||||
guard = get_guard()
|
||||
|
||||
if not guard.is_locked():
|
||||
# Counter hängt! Process Guard ist frei, aber Counter sagt Browser läuft
|
||||
logger.warning(
|
||||
f"⚠️ SAFETY-RESET: _active_count war {PlaywrightManager._active_count}, "
|
||||
f"aber ProcessGuard ist nicht locked. Counter wird zurückgesetzt."
|
||||
)
|
||||
PlaywrightManager._active_count = 0
|
||||
else:
|
||||
# Guard ist locked UND Counter ist > 0 → echte parallele Instanz
|
||||
raise RuntimeError(
|
||||
"Browser bereits aktiv. Nur eine Browser-Instanz gleichzeitig erlaubt. "
|
||||
"Beenden Sie den aktuellen Prozess."
|
||||
)
|
||||
|
||||
try:
|
||||
self.playwright = sync_playwright().start()
|
||||
@ -257,6 +277,10 @@ class PlaywrightManager:
|
||||
# Event-Listener für Konsolen-Logs
|
||||
self.page.on("console", lambda msg: logger.debug(f"BROWSER CONSOLE: {msg.text}"))
|
||||
|
||||
# Feature 5: Browser-Disconnect-Handler registrieren (für manuelles Schließen)
|
||||
self.browser.on("disconnected", self._on_browser_disconnected)
|
||||
logger.debug("Browser-Disconnect-Handler registriert")
|
||||
|
||||
# Feature 5: Browser-Instanz Counter erhöhen
|
||||
PlaywrightManager._active_count += 1
|
||||
logger.info(f"Browser gestartet (aktive Instanzen: {PlaywrightManager._active_count})")
|
||||
@ -966,7 +990,33 @@ class PlaywrightManager:
|
||||
self.protection_applied = False
|
||||
self.protection_style = None
|
||||
logger.info("Browser-Schutz entfernt")
|
||||
|
||||
|
||||
def _on_browser_disconnected(self):
|
||||
"""
|
||||
Event-Handler: Wird aufgerufen wenn Browser-Verbindung getrennt wird.
|
||||
|
||||
Dekrementiert den aktiven Browser-Counter, auch wenn der Browser
|
||||
manuell geschlossen wurde (z.B. durch User-Klick auf X-Button).
|
||||
|
||||
Dies stellt sicher dass der Missbrauchs-Schutz korrekt funktioniert,
|
||||
selbst wenn close() nicht explizit aufgerufen wird.
|
||||
"""
|
||||
# Verhindere doppelte Dekrementierung
|
||||
if self._browser_was_disconnected:
|
||||
logger.debug("Browser bereits als disconnected markiert, überspringe Counter-Dekrementierung")
|
||||
return
|
||||
|
||||
self._browser_was_disconnected = True
|
||||
|
||||
if PlaywrightManager._active_count > 0:
|
||||
PlaywrightManager._active_count -= 1
|
||||
logger.info(
|
||||
f"🔌 Browser disconnected - Counter dekrementiert "
|
||||
f"(aktive Instanzen: {PlaywrightManager._active_count})"
|
||||
)
|
||||
else:
|
||||
logger.warning("Browser disconnected aber Counter war bereits 0")
|
||||
|
||||
def close(self):
|
||||
"""Schließt den Browser und gibt Ressourcen frei."""
|
||||
try:
|
||||
@ -1012,9 +1062,15 @@ class PlaywrightManager:
|
||||
self.playwright = None
|
||||
|
||||
# Feature 5: Browser-Instanz Counter dekrementieren
|
||||
if PlaywrightManager._active_count > 0:
|
||||
PlaywrightManager._active_count -= 1
|
||||
logger.info(f"Browser geschlossen (aktive Instanzen: {PlaywrightManager._active_count})")
|
||||
# (nur wenn nicht bereits durch disconnected-Event dekrementiert)
|
||||
if not self._browser_was_disconnected:
|
||||
if PlaywrightManager._active_count > 0:
|
||||
PlaywrightManager._active_count -= 1
|
||||
logger.info(f"Browser geschlossen (aktive Instanzen: {PlaywrightManager._active_count})")
|
||||
else:
|
||||
logger.warning("Browser-Counter war bereits 0 beim Schließen")
|
||||
else:
|
||||
logger.debug("Counter wurde bereits durch disconnected-Event dekrementiert")
|
||||
|
||||
logger.info("Browser-Sitzung erfolgreich geschlossen")
|
||||
|
||||
|
||||
@ -105,80 +105,100 @@ class FacebookController(BasePlatformController):
|
||||
# Validiere Eingaben
|
||||
is_valid, error_msg = self.validate_inputs(params)
|
||||
if not is_valid:
|
||||
# Guard freigeben da Worker nicht gestartet wird
|
||||
from utils.process_guard import get_guard
|
||||
get_guard().end(success=False)
|
||||
self.get_generator_tab().show_error(error_msg)
|
||||
return
|
||||
|
||||
# UI aktualisieren
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(True)
|
||||
if hasattr(generator_tab, 'clear_log'):
|
||||
generator_tab.clear_log()
|
||||
if hasattr(generator_tab, 'set_progress'):
|
||||
generator_tab.set_progress(0)
|
||||
|
||||
# Schmiedeanimation-Dialog erstellen und anzeigen
|
||||
parent_widget = generator_tab.window() # Hauptfenster als Parent
|
||||
self.forge_dialog = ForgeAnimationDialog(parent_widget, "Facebook")
|
||||
self.forge_dialog.cancel_clicked.connect(self.stop_account_creation)
|
||||
self.forge_dialog.closed.connect(self.stop_account_creation)
|
||||
|
||||
# Fensterposition vom Hauptfenster holen
|
||||
if parent_widget:
|
||||
window_pos = parent_widget.pos()
|
||||
params["window_position"] = (window_pos.x(), window_pos.y())
|
||||
|
||||
# Fingerprint VOR Account-Erstellung generieren
|
||||
try:
|
||||
from infrastructure.services.fingerprint.fingerprint_generator_service import FingerprintGeneratorService
|
||||
from domain.entities.browser_fingerprint import BrowserFingerprint
|
||||
import uuid
|
||||
# UI aktualisieren
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(True)
|
||||
if hasattr(generator_tab, 'clear_log'):
|
||||
generator_tab.clear_log()
|
||||
if hasattr(generator_tab, 'set_progress'):
|
||||
generator_tab.set_progress(0)
|
||||
|
||||
fingerprint_service = FingerprintGeneratorService()
|
||||
# Schmiedeanimation-Dialog erstellen und anzeigen
|
||||
parent_widget = generator_tab.window() # Hauptfenster als Parent
|
||||
self.forge_dialog = ForgeAnimationDialog(parent_widget, "Facebook")
|
||||
self.forge_dialog.cancel_clicked.connect(self.stop_account_creation)
|
||||
self.forge_dialog.closed.connect(self.stop_account_creation)
|
||||
|
||||
# Generiere einen neuen Fingerprint für diesen Account
|
||||
fingerprint_data = fingerprint_service.generate_fingerprint()
|
||||
# Fensterposition vom Hauptfenster holen
|
||||
if parent_widget:
|
||||
window_pos = parent_widget.pos()
|
||||
params["window_position"] = (window_pos.x(), window_pos.y())
|
||||
|
||||
# Erstelle BrowserFingerprint Entity mit allen notwendigen Daten
|
||||
fingerprint = BrowserFingerprint.from_dict(fingerprint_data)
|
||||
fingerprint.fingerprint_id = str(uuid.uuid4())
|
||||
fingerprint.account_bound = True
|
||||
fingerprint.rotation_seed = str(uuid.uuid4())
|
||||
# Fingerprint VOR Account-Erstellung generieren
|
||||
try:
|
||||
from infrastructure.services.fingerprint.fingerprint_generator_service import FingerprintGeneratorService
|
||||
from domain.entities.browser_fingerprint import BrowserFingerprint
|
||||
import uuid
|
||||
|
||||
# Konvertiere zu Dictionary für Übertragung
|
||||
params["fingerprint"] = fingerprint.to_dict()
|
||||
fingerprint_service = FingerprintGeneratorService()
|
||||
|
||||
# Generiere einen neuen Fingerprint für diesen Account
|
||||
fingerprint_data = fingerprint_service.generate_fingerprint()
|
||||
|
||||
# Erstelle BrowserFingerprint Entity mit allen notwendigen Daten
|
||||
fingerprint = BrowserFingerprint.from_dict(fingerprint_data)
|
||||
fingerprint.fingerprint_id = str(uuid.uuid4())
|
||||
fingerprint.account_bound = True
|
||||
fingerprint.rotation_seed = str(uuid.uuid4())
|
||||
|
||||
# Konvertiere zu Dictionary für Übertragung
|
||||
params["fingerprint"] = fingerprint.to_dict()
|
||||
|
||||
logger.info(f"Fingerprint für neue Account-Erstellung generiert: {fingerprint.fingerprint_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Generieren des Fingerprints: {e}")
|
||||
# Fortfahren ohne Fingerprint - wird später generiert
|
||||
|
||||
# Worker-Thread starten mit optionalen Parametern
|
||||
session_controller = getattr(self, 'session_controller', None)
|
||||
generator_tab_ref = generator_tab if hasattr(generator_tab, 'store_created_account') else None
|
||||
|
||||
self.worker_thread = FacebookWorkerThread(
|
||||
params,
|
||||
session_controller=session_controller,
|
||||
generator_tab=generator_tab_ref
|
||||
)
|
||||
|
||||
# Updates an Forge-Dialog weiterleiten
|
||||
self.worker_thread.update_signal.connect(self.forge_dialog.set_status)
|
||||
self.worker_thread.log_signal.connect(self.forge_dialog.add_log)
|
||||
self.worker_thread.error_signal.connect(self._handle_error)
|
||||
self.worker_thread.finished_signal.connect(self._handle_finished)
|
||||
self.worker_thread.progress_signal.connect(self.forge_dialog.set_progress)
|
||||
|
||||
# Auch an Generator-Tab für Backup
|
||||
self.worker_thread.log_signal.connect(lambda msg: generator_tab.add_log(msg))
|
||||
if hasattr(generator_tab, 'set_progress'):
|
||||
self.worker_thread.progress_signal.connect(lambda value: generator_tab.set_progress(value))
|
||||
|
||||
self.worker_thread.start()
|
||||
|
||||
# Dialog anzeigen und Animation starten
|
||||
self.forge_dialog.start_animation()
|
||||
self.forge_dialog.show()
|
||||
|
||||
logger.info(f"Fingerprint für neue Account-Erstellung generiert: {fingerprint.fingerprint_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Generieren des Fingerprints: {e}")
|
||||
# Fortfahren ohne Fingerprint - wird später generiert
|
||||
# Kritischer Fehler VOR Worker-Start → Guard freigeben!
|
||||
logger.error(f"Fehler beim Start der Account-Erstellung: {e}", exc_info=True)
|
||||
|
||||
# Worker-Thread starten mit optionalen Parametern
|
||||
session_controller = getattr(self, 'session_controller', None)
|
||||
generator_tab_ref = generator_tab if hasattr(generator_tab, 'store_created_account') else None
|
||||
from utils.process_guard import get_guard
|
||||
get_guard().end(success=False)
|
||||
|
||||
self.worker_thread = FacebookWorkerThread(
|
||||
params,
|
||||
session_controller=session_controller,
|
||||
generator_tab=generator_tab_ref
|
||||
)
|
||||
# Dialog schließen falls vorhanden
|
||||
if hasattr(self, 'forge_dialog') and self.forge_dialog:
|
||||
self.forge_dialog.close()
|
||||
|
||||
# Updates an Forge-Dialog weiterleiten
|
||||
self.worker_thread.update_signal.connect(self.forge_dialog.set_status)
|
||||
self.worker_thread.log_signal.connect(self.forge_dialog.add_log)
|
||||
self.worker_thread.error_signal.connect(self._handle_error)
|
||||
self.worker_thread.finished_signal.connect(self._handle_finished)
|
||||
self.worker_thread.progress_signal.connect(self.forge_dialog.set_progress)
|
||||
|
||||
# Auch an Generator-Tab für Backup
|
||||
self.worker_thread.log_signal.connect(lambda msg: generator_tab.add_log(msg))
|
||||
if hasattr(generator_tab, 'set_progress'):
|
||||
self.worker_thread.progress_signal.connect(lambda value: generator_tab.set_progress(value))
|
||||
|
||||
self.worker_thread.start()
|
||||
|
||||
# Dialog anzeigen und Animation starten
|
||||
self.forge_dialog.start_animation()
|
||||
self.forge_dialog.show()
|
||||
# UI zurücksetzen
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(False)
|
||||
generator_tab.show_error(f"Fehler beim Start: {str(e)}")
|
||||
|
||||
def handle_account_created(self, result: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
|
||||
@ -197,76 +197,96 @@ class InstagramController(BasePlatformController):
|
||||
# Validiere Eingaben
|
||||
is_valid, error_msg = self.validate_inputs(params)
|
||||
if not is_valid:
|
||||
# Guard freigeben da Worker nicht gestartet wird
|
||||
from utils.process_guard import get_guard
|
||||
get_guard().end(success=False)
|
||||
self.get_generator_tab().show_error(error_msg)
|
||||
return
|
||||
|
||||
# UI aktualisieren
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(True)
|
||||
generator_tab.clear_log()
|
||||
generator_tab.set_progress(0)
|
||||
|
||||
# Schmiedeanimation-Dialog erstellen und anzeigen
|
||||
parent_widget = generator_tab.window() # Hauptfenster als Parent
|
||||
self.forge_dialog = ForgeAnimationDialog(parent_widget, "Instagram")
|
||||
self.forge_dialog.cancel_clicked.connect(self.stop_account_creation)
|
||||
self.forge_dialog.closed.connect(self.stop_account_creation)
|
||||
|
||||
# Fensterposition vom Hauptfenster holen
|
||||
if parent_widget:
|
||||
window_pos = parent_widget.pos()
|
||||
params["window_position"] = (window_pos.x(), window_pos.y())
|
||||
|
||||
# Fingerprint VOR Account-Erstellung generieren
|
||||
try:
|
||||
from infrastructure.services.fingerprint.fingerprint_generator_service import FingerprintGeneratorService
|
||||
from domain.entities.browser_fingerprint import BrowserFingerprint
|
||||
import uuid
|
||||
|
||||
fingerprint_service = FingerprintGeneratorService()
|
||||
|
||||
# Generiere einen neuen Fingerprint für diesen Account
|
||||
fingerprint_data = fingerprint_service.generate_fingerprint()
|
||||
|
||||
# Erstelle BrowserFingerprint Entity mit allen notwendigen Daten
|
||||
fingerprint = BrowserFingerprint.from_dict(fingerprint_data)
|
||||
fingerprint.fingerprint_id = str(uuid.uuid4())
|
||||
fingerprint.account_bound = True
|
||||
fingerprint.rotation_seed = str(uuid.uuid4())
|
||||
|
||||
# Konvertiere zu Dictionary für Übertragung
|
||||
params["fingerprint"] = fingerprint.to_dict()
|
||||
|
||||
logger.info(f"Fingerprint für neue Account-Erstellung generiert: {fingerprint.fingerprint_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Generieren des Fingerprints: {e}")
|
||||
# Fortfahren ohne Fingerprint - wird später generiert
|
||||
|
||||
# Worker-Thread starten mit optionalen Parametern
|
||||
session_controller = getattr(self, 'session_controller', None)
|
||||
generator_tab_ref = generator_tab if hasattr(generator_tab, 'store_created_account') else None
|
||||
|
||||
self.worker_thread = InstagramWorkerThread(
|
||||
params,
|
||||
session_controller=session_controller,
|
||||
generator_tab=generator_tab_ref
|
||||
)
|
||||
# Updates an Forge-Dialog weiterleiten
|
||||
self.worker_thread.update_signal.connect(self.forge_dialog.set_status)
|
||||
self.worker_thread.log_signal.connect(self.forge_dialog.add_log)
|
||||
self.worker_thread.error_signal.connect(self._handle_error)
|
||||
self.worker_thread.finished_signal.connect(self._handle_finished)
|
||||
self.worker_thread.progress_signal.connect(self.forge_dialog.set_progress)
|
||||
|
||||
# Auch an Generator-Tab für Backup
|
||||
self.worker_thread.log_signal.connect(lambda msg: generator_tab.add_log(msg))
|
||||
self.worker_thread.progress_signal.connect(lambda value: generator_tab.set_progress(value))
|
||||
# UI aktualisieren
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(True)
|
||||
generator_tab.clear_log()
|
||||
generator_tab.set_progress(0)
|
||||
|
||||
self.worker_thread.start()
|
||||
|
||||
# Dialog anzeigen und Animation starten
|
||||
self.forge_dialog.start_animation()
|
||||
self.forge_dialog.show()
|
||||
# Schmiedeanimation-Dialog erstellen und anzeigen
|
||||
parent_widget = generator_tab.window() # Hauptfenster als Parent
|
||||
self.forge_dialog = ForgeAnimationDialog(parent_widget, "Instagram")
|
||||
self.forge_dialog.cancel_clicked.connect(self.stop_account_creation)
|
||||
self.forge_dialog.closed.connect(self.stop_account_creation)
|
||||
|
||||
# Fensterposition vom Hauptfenster holen
|
||||
if parent_widget:
|
||||
window_pos = parent_widget.pos()
|
||||
params["window_position"] = (window_pos.x(), window_pos.y())
|
||||
|
||||
# Fingerprint VOR Account-Erstellung generieren
|
||||
try:
|
||||
from infrastructure.services.fingerprint.fingerprint_generator_service import FingerprintGeneratorService
|
||||
from domain.entities.browser_fingerprint import BrowserFingerprint
|
||||
import uuid
|
||||
|
||||
fingerprint_service = FingerprintGeneratorService()
|
||||
|
||||
# Generiere einen neuen Fingerprint für diesen Account
|
||||
fingerprint_data = fingerprint_service.generate_fingerprint()
|
||||
|
||||
# Erstelle BrowserFingerprint Entity mit allen notwendigen Daten
|
||||
fingerprint = BrowserFingerprint.from_dict(fingerprint_data)
|
||||
fingerprint.fingerprint_id = str(uuid.uuid4())
|
||||
fingerprint.account_bound = True
|
||||
fingerprint.rotation_seed = str(uuid.uuid4())
|
||||
|
||||
# Konvertiere zu Dictionary für Übertragung
|
||||
params["fingerprint"] = fingerprint.to_dict()
|
||||
|
||||
logger.info(f"Fingerprint für neue Account-Erstellung generiert: {fingerprint.fingerprint_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Generieren des Fingerprints: {e}")
|
||||
# Fortfahren ohne Fingerprint - wird später generiert
|
||||
|
||||
# Worker-Thread starten mit optionalen Parametern
|
||||
session_controller = getattr(self, 'session_controller', None)
|
||||
generator_tab_ref = generator_tab if hasattr(generator_tab, 'store_created_account') else None
|
||||
|
||||
self.worker_thread = InstagramWorkerThread(
|
||||
params,
|
||||
session_controller=session_controller,
|
||||
generator_tab=generator_tab_ref
|
||||
)
|
||||
# Updates an Forge-Dialog weiterleiten
|
||||
self.worker_thread.update_signal.connect(self.forge_dialog.set_status)
|
||||
self.worker_thread.log_signal.connect(self.forge_dialog.add_log)
|
||||
self.worker_thread.error_signal.connect(self._handle_error)
|
||||
self.worker_thread.finished_signal.connect(self._handle_finished)
|
||||
self.worker_thread.progress_signal.connect(self.forge_dialog.set_progress)
|
||||
|
||||
# Auch an Generator-Tab für Backup
|
||||
self.worker_thread.log_signal.connect(lambda msg: generator_tab.add_log(msg))
|
||||
self.worker_thread.progress_signal.connect(lambda value: generator_tab.set_progress(value))
|
||||
|
||||
self.worker_thread.start()
|
||||
|
||||
# Dialog anzeigen und Animation starten
|
||||
self.forge_dialog.start_animation()
|
||||
self.forge_dialog.show()
|
||||
|
||||
except Exception as e:
|
||||
# Kritischer Fehler VOR Worker-Start → Guard freigeben!
|
||||
logger.error(f"Fehler beim Start der Account-Erstellung: {e}", exc_info=True)
|
||||
|
||||
from utils.process_guard import get_guard
|
||||
get_guard().end(success=False)
|
||||
|
||||
# Dialog schließen falls vorhanden
|
||||
if hasattr(self, 'forge_dialog') and self.forge_dialog:
|
||||
self.forge_dialog.close()
|
||||
|
||||
# UI zurücksetzen
|
||||
generator_tab = self.get_generator_tab()
|
||||
generator_tab.set_running(False)
|
||||
generator_tab.show_error(f"Fehler beim Start: {str(e)}")
|
||||
|
||||
def stop_account_creation(self):
|
||||
"""Stoppt die Instagram-Account-Erstellung mit Guard-Freigabe."""
|
||||
|
||||
@ -274,40 +274,7 @@ class ProfileExportController:
|
||||
logger.error(f"Fehler beim Export von {username}: {e}")
|
||||
failed_accounts.append(username)
|
||||
|
||||
# 5. Summary-Datei erstellen
|
||||
summary_path = os.path.join(save_directory, "export_summary.txt")
|
||||
with open(summary_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"AccountForge Batch-Export\n")
|
||||
f.write(f"="*50 + "\n\n")
|
||||
f.write(f"Exportiert am: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}\n")
|
||||
f.write(f"Anzahl Accounts: {len(accounts_data)}\n")
|
||||
f.write(f"Erfolgreich: {len(accounts_data) - len(failed_accounts)}\n")
|
||||
if failed_accounts:
|
||||
f.write(f"Fehlgeschlagen: {len(failed_accounts)}\n")
|
||||
f.write(f"\nFormate: {', '.join(formats).upper()}\n")
|
||||
f.write(f"\n" + "="*50 + "\n\n")
|
||||
|
||||
# Gruppiere nach Plattform
|
||||
platforms = {}
|
||||
for account_data in accounts_data:
|
||||
platform = account_data.get("platform", "unknown").lower()
|
||||
if platform not in platforms:
|
||||
platforms[platform] = []
|
||||
platforms[platform].append(account_data.get("username", ""))
|
||||
|
||||
for platform, usernames in sorted(platforms.items()):
|
||||
f.write(f"{platform.capitalize()}:\n")
|
||||
for username in usernames:
|
||||
if username in failed_accounts:
|
||||
f.write(f" ✗ {username} (FEHLER)\n")
|
||||
else:
|
||||
f.write(f" ✓ {username}\n")
|
||||
f.write(f"\n")
|
||||
|
||||
exported_files.append("export_summary.txt")
|
||||
logger.info("Summary-Datei erstellt")
|
||||
|
||||
# 6. Erfolgs-Dialog anzeigen
|
||||
# 5. Erfolgs-Dialog anzeigen
|
||||
show_export_success(
|
||||
parent_widget,
|
||||
exported_files,
|
||||
|
||||
Binäre Datei nicht angezeigt.
158
run_migration.py
158
run_migration.py
@ -1,158 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Run method rotation system database migration.
|
||||
This script applies the rotation system database schema to the existing database.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import sqlite3
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_migration():
|
||||
"""Run the method rotation system migration"""
|
||||
try:
|
||||
# Database path
|
||||
db_path = project_root / "database" / "accounts.db"
|
||||
migration_path = project_root / "database" / "migrations" / "add_method_rotation_system.sql"
|
||||
|
||||
if not db_path.exists():
|
||||
logger.error(f"Database not found at {db_path}")
|
||||
return False
|
||||
|
||||
if not migration_path.exists():
|
||||
logger.error(f"Migration file not found at {migration_path}")
|
||||
return False
|
||||
|
||||
# Read migration SQL
|
||||
with open(migration_path, 'r', encoding='utf-8') as f:
|
||||
migration_sql = f.read()
|
||||
|
||||
# Connect to database
|
||||
logger.info(f"Connecting to database at {db_path}")
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
|
||||
try:
|
||||
# Check if migration has already been run
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='method_strategies'")
|
||||
if cursor.fetchone():
|
||||
logger.info("Method rotation tables already exist, skipping migration")
|
||||
return True
|
||||
|
||||
# Run migration
|
||||
logger.info("Running method rotation system migration...")
|
||||
conn.executescript(migration_sql)
|
||||
conn.commit()
|
||||
|
||||
# Verify migration
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%method%' OR name LIKE '%rotation%'")
|
||||
tables = cursor.fetchall()
|
||||
|
||||
expected_tables = [
|
||||
'method_strategies',
|
||||
'rotation_sessions',
|
||||
'rotation_events',
|
||||
'method_performance_analytics',
|
||||
'method_cooldowns',
|
||||
'platform_method_states'
|
||||
]
|
||||
|
||||
created_tables = [table[0] for table in tables]
|
||||
missing_tables = [t for t in expected_tables if t not in created_tables]
|
||||
|
||||
if missing_tables:
|
||||
logger.error(f"Migration incomplete, missing tables: {missing_tables}")
|
||||
return False
|
||||
|
||||
logger.info(f"Migration successful! Created tables: {created_tables}")
|
||||
|
||||
# Verify default data was inserted
|
||||
cursor.execute("SELECT COUNT(*) FROM method_strategies")
|
||||
strategy_count = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM platform_method_states")
|
||||
state_count = cursor.fetchone()[0]
|
||||
|
||||
logger.info(f"Default data inserted: {strategy_count} strategies, {state_count} platform states")
|
||||
|
||||
return True
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Migration failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def check_migration_status():
|
||||
"""Check if migration has been applied"""
|
||||
try:
|
||||
db_path = project_root / "database" / "accounts.db"
|
||||
|
||||
if not db_path.exists():
|
||||
logger.warning(f"Database not found at {db_path}")
|
||||
return False
|
||||
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check for rotation tables
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND (
|
||||
name LIKE '%method%' OR
|
||||
name LIKE '%rotation%'
|
||||
)
|
||||
ORDER BY name
|
||||
""")
|
||||
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
|
||||
if tables:
|
||||
logger.info(f"Method rotation tables found: {tables}")
|
||||
return True
|
||||
else:
|
||||
logger.info("No method rotation tables found")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check migration status: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
logger.info("Method Rotation System Database Migration")
|
||||
logger.info("=" * 50)
|
||||
|
||||
# Check current status
|
||||
logger.info("Checking current migration status...")
|
||||
if check_migration_status():
|
||||
logger.info("Migration already applied")
|
||||
return
|
||||
|
||||
# Run migration
|
||||
logger.info("Applying migration...")
|
||||
if run_migration():
|
||||
logger.info("Migration completed successfully!")
|
||||
logger.info("Method rotation system is now ready to use")
|
||||
else:
|
||||
logger.error("Migration failed!")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug script to check what's happening with logo switching
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from themes.theme_config import ThemeConfig
|
||||
|
||||
def check_logo_files():
|
||||
"""Check the actual logo files and their paths."""
|
||||
print("=" * 60)
|
||||
print("LOGO FILE ANALYSIS")
|
||||
print("=" * 60)
|
||||
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Check what's in the config
|
||||
light_theme = ThemeConfig.get_theme('light')
|
||||
dark_theme = ThemeConfig.get_theme('dark')
|
||||
|
||||
print(f"\nTheme Config:")
|
||||
print(f" Light theme logo_path: {light_theme.get('logo_path', 'NOT SET')}")
|
||||
print(f" Dark theme logo_path: {dark_theme.get('logo_path', 'NOT SET')}")
|
||||
|
||||
# Check actual files
|
||||
icons_dir = os.path.join(base_dir, "resources", "icons")
|
||||
print(f"\nIcon files in {icons_dir}:")
|
||||
|
||||
for file in os.listdir(icons_dir):
|
||||
if "intelsight" in file.lower():
|
||||
file_path = os.path.join(icons_dir, file)
|
||||
file_size = os.path.getsize(file_path)
|
||||
print(f" - {file} ({file_size} bytes)")
|
||||
|
||||
# Test the path resolution
|
||||
print("\n" + "=" * 60)
|
||||
print("PATH RESOLUTION TEST")
|
||||
print("=" * 60)
|
||||
|
||||
# Simulate what happens in get_icon_path
|
||||
for theme_name in ['light', 'dark']:
|
||||
theme = ThemeConfig.get_theme(theme_name)
|
||||
logo_name = theme.get('logo_path', 'intelsight-logo.svg').replace('.svg', '')
|
||||
full_path = os.path.join(base_dir, "resources", "icons", f"{logo_name}.svg")
|
||||
|
||||
print(f"\n{theme_name.upper()} theme:")
|
||||
print(f" logo_name from config: {logo_name}")
|
||||
print(f" full_path: {full_path}")
|
||||
print(f" file exists: {os.path.exists(full_path)}")
|
||||
|
||||
if os.path.exists(full_path):
|
||||
# Check if it's actually an SVG
|
||||
with open(full_path, 'r') as f:
|
||||
first_line = f.readline().strip()
|
||||
is_svg = '<svg' in first_line.lower() or '<?xml' in first_line.lower()
|
||||
print(f" is valid SVG: {is_svg}")
|
||||
print(f" first line: {first_line[:50]}...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_logo_files()
|
||||
@ -1,88 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify logo switching logic without PyQt5
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from themes.theme_config import ThemeConfig
|
||||
|
||||
def test_logo_paths():
|
||||
"""Test that logo paths are correctly configured."""
|
||||
print("=" * 60)
|
||||
print("LOGO SWITCHING TEST")
|
||||
print("=" * 60)
|
||||
|
||||
# Test light theme logo
|
||||
light_theme = ThemeConfig.get_theme('light')
|
||||
light_logo = light_theme.get('logo_path', 'NOT_FOUND')
|
||||
print(f"\n✓ Light theme logo: {light_logo}")
|
||||
|
||||
# Test dark theme logo
|
||||
dark_theme = ThemeConfig.get_theme('dark')
|
||||
dark_logo = dark_theme.get('logo_path', 'NOT_FOUND')
|
||||
print(f"✓ Dark theme logo: {dark_logo}")
|
||||
|
||||
# Check if files exist
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
light_path = os.path.join(base_dir, "resources", "icons", light_logo)
|
||||
dark_path = os.path.join(base_dir, "resources", "icons", dark_logo)
|
||||
|
||||
print(f"\n✓ Light logo exists: {os.path.exists(light_path)} ({light_path})")
|
||||
print(f"✓ Dark logo exists: {os.path.exists(dark_path)} ({dark_path})")
|
||||
|
||||
# Simulate theme manager logic
|
||||
print("\n" + "=" * 60)
|
||||
print("SIMULATING THEME MANAGER LOGIC")
|
||||
print("=" * 60)
|
||||
|
||||
class MockThemeManager:
|
||||
def __init__(self):
|
||||
self.base_dir = base_dir
|
||||
self.current_theme = 'light'
|
||||
|
||||
def get_icon_path(self, icon_name):
|
||||
if icon_name == "intelsight-logo":
|
||||
theme = ThemeConfig.get_theme(self.current_theme)
|
||||
logo_name = theme.get('logo_path', 'intelsight-logo.svg').replace('.svg', '')
|
||||
return os.path.join(self.base_dir, "resources", "icons", f"{logo_name}.svg")
|
||||
return os.path.join(self.base_dir, "resources", "icons", f"{icon_name}.svg")
|
||||
|
||||
tm = MockThemeManager()
|
||||
|
||||
# Test light theme
|
||||
tm.current_theme = 'light'
|
||||
light_result = tm.get_icon_path("intelsight-logo")
|
||||
print(f"\nLight theme path: {light_result}")
|
||||
print(f"File exists: {os.path.exists(light_result)}")
|
||||
|
||||
# Test dark theme
|
||||
tm.current_theme = 'dark'
|
||||
dark_result = tm.get_icon_path("intelsight-logo")
|
||||
print(f"\nDark theme path: {dark_result}")
|
||||
print(f"File exists: {os.path.exists(dark_result)}")
|
||||
|
||||
# Check if paths are different
|
||||
if light_result != dark_result:
|
||||
print("\n✅ SUCCESS: Different logos for different themes!")
|
||||
else:
|
||||
print("\n❌ ERROR: Same logo path for both themes!")
|
||||
return False
|
||||
|
||||
# Check actual file names
|
||||
if "intelsight-logo" in light_result and "intelsight-dark" in dark_result:
|
||||
print("✅ SUCCESS: Correct logo files selected!")
|
||||
else:
|
||||
print("❌ ERROR: Wrong logo files!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_logo_paths()
|
||||
sys.exit(0 if success else 1)
|
||||
@ -1,279 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive test script for the theme system refactoring.
|
||||
Tests all components without requiring PyQt5.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def test_theme_config():
|
||||
"""Test theme configuration module."""
|
||||
print("\n=== Testing Theme Configuration ===")
|
||||
try:
|
||||
from themes.theme_config import ThemeConfig
|
||||
|
||||
# Test light theme
|
||||
light = ThemeConfig.get_theme('light')
|
||||
print(f"✓ Light theme loaded: {len(light)} color definitions")
|
||||
|
||||
# Test dark theme
|
||||
dark = ThemeConfig.get_theme('dark')
|
||||
print(f"✓ Dark theme loaded: {len(dark)} color definitions")
|
||||
|
||||
# Check critical keys
|
||||
critical_keys = [
|
||||
'bg_primary', 'bg_secondary', 'bg_tertiary',
|
||||
'text_primary', 'text_secondary', 'text_tertiary',
|
||||
'accent', 'accent_hover', 'accent_pressed',
|
||||
'error', 'error_dark', 'success', 'warning',
|
||||
'border_default', 'border_subtle',
|
||||
'logo_path'
|
||||
]
|
||||
|
||||
for key in critical_keys:
|
||||
if key not in light:
|
||||
print(f"✗ Missing in light theme: {key}")
|
||||
return False
|
||||
if key not in dark:
|
||||
print(f"✗ Missing in dark theme: {key}")
|
||||
return False
|
||||
|
||||
print(f"✓ All {len(critical_keys)} critical keys present in both themes")
|
||||
|
||||
# Test sizes, fonts, etc
|
||||
sizes = ThemeConfig.FONT_SIZES
|
||||
fonts = ThemeConfig.FONTS
|
||||
weights = ThemeConfig.FONT_WEIGHTS
|
||||
spacing = ThemeConfig.SPACING
|
||||
radius = ThemeConfig.RADIUS
|
||||
|
||||
print(f"✓ Font sizes defined: {list(sizes.keys())}")
|
||||
print(f"✓ Font families defined: {list(fonts.keys())}")
|
||||
print(f"✓ Font weights defined: {list(weights.keys())}")
|
||||
print(f"✓ Spacing values defined: {list(spacing.keys())}")
|
||||
print(f"✓ Border radius defined: {list(radius.keys())}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error testing theme config: {e}")
|
||||
return False
|
||||
|
||||
def test_qss_generator():
|
||||
"""Test QSS generation."""
|
||||
print("\n=== Testing QSS Generator ===")
|
||||
try:
|
||||
from themes.qss_generator import QSSGenerator
|
||||
|
||||
# Generate light theme QSS
|
||||
light_qss = QSSGenerator.generate('light')
|
||||
print(f"✓ Light theme QSS generated: {len(light_qss)} characters")
|
||||
|
||||
# Generate dark theme QSS
|
||||
dark_qss = QSSGenerator.generate('dark')
|
||||
print(f"✓ Dark theme QSS generated: {len(dark_qss)} characters")
|
||||
|
||||
# Check for key selectors
|
||||
key_selectors = [
|
||||
'QMainWindow', 'QPushButton', 'QLabel', 'QLineEdit',
|
||||
'QTextEdit', 'QScrollArea', 'QFrame', 'QWidget',
|
||||
'QMenuBar', 'QTabBar', 'QDialog'
|
||||
]
|
||||
|
||||
missing_light = []
|
||||
missing_dark = []
|
||||
|
||||
for selector in key_selectors:
|
||||
if selector not in light_qss:
|
||||
missing_light.append(selector)
|
||||
if selector not in dark_qss:
|
||||
missing_dark.append(selector)
|
||||
|
||||
if missing_light:
|
||||
print(f"✗ Missing selectors in light QSS: {missing_light}")
|
||||
if missing_dark:
|
||||
print(f"✗ Missing selectors in dark QSS: {missing_dark}")
|
||||
|
||||
if not missing_light and not missing_dark:
|
||||
print(f"✓ All {len(key_selectors)} key selectors present in both themes")
|
||||
|
||||
# Check for object name selectors (our custom ones)
|
||||
custom_selectors = [
|
||||
'#platform_button', '#filter_button', '#account_username',
|
||||
'#account_login_btn', '#account_export_btn', '#account_delete_btn',
|
||||
'#logo_button', '#dark_mode_toggle'
|
||||
]
|
||||
|
||||
found_custom = []
|
||||
for selector in custom_selectors:
|
||||
if selector in light_qss:
|
||||
found_custom.append(selector)
|
||||
|
||||
print(f"✓ Custom selectors found: {len(found_custom)}/{len(custom_selectors)}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error testing QSS generator: {e}")
|
||||
return False
|
||||
|
||||
def test_file_structure():
|
||||
"""Test that all required files exist."""
|
||||
print("\n=== Testing File Structure ===")
|
||||
|
||||
required_files = [
|
||||
'themes/__init__.py',
|
||||
'themes/theme_config.py',
|
||||
'themes/qss_generator.py',
|
||||
'views/base/__init__.py',
|
||||
'views/base/theme_aware_widget.py',
|
||||
'views/widgets/dark_mode_toggle.py',
|
||||
'utils/theme_manager.py',
|
||||
'resources/icons/intelsight-logo.svg',
|
||||
'resources/icons/intelsight-dark.svg'
|
||||
]
|
||||
|
||||
missing = []
|
||||
for file in required_files:
|
||||
path = Path(file)
|
||||
if path.exists():
|
||||
print(f"✓ {file}")
|
||||
else:
|
||||
print(f"✗ Missing: {file}")
|
||||
missing.append(file)
|
||||
|
||||
if missing:
|
||||
print(f"\n✗ Missing {len(missing)} required files")
|
||||
return False
|
||||
else:
|
||||
print(f"\n✓ All {len(required_files)} required files present")
|
||||
return True
|
||||
|
||||
def test_no_hardcoded_colors():
|
||||
"""Check for hardcoded colors in view files."""
|
||||
print("\n=== Checking for Hardcoded Colors ===")
|
||||
|
||||
import re
|
||||
|
||||
# Pattern to match hex colors
|
||||
hex_pattern = re.compile(r'#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}')
|
||||
rgb_pattern = re.compile(r'rgb\s*\([^)]+\)|rgba\s*\([^)]+\)')
|
||||
|
||||
view_files = [
|
||||
'views/main_window.py',
|
||||
'views/platform_selector.py',
|
||||
'views/components/tab_navigation.py',
|
||||
'views/components/platform_grid_view.py',
|
||||
'views/components/accounts_overview_view.py',
|
||||
'views/widgets/platform_button.py',
|
||||
'views/widgets/account_card.py'
|
||||
]
|
||||
|
||||
files_with_colors = []
|
||||
|
||||
for file in view_files:
|
||||
if not Path(file).exists():
|
||||
continue
|
||||
|
||||
with open(file, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Skip import statements and comments
|
||||
lines = content.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
# Skip comments and imports
|
||||
if line.strip().startswith('#') or line.strip().startswith('from') or line.strip().startswith('import'):
|
||||
continue
|
||||
|
||||
# Check for hex colors
|
||||
if 'setStyleSheet' in line and (hex_pattern.search(line) or rgb_pattern.search(line)):
|
||||
files_with_colors.append((file, i+1, line.strip()))
|
||||
|
||||
if files_with_colors:
|
||||
print(f"✗ Found {len(files_with_colors)} lines with hardcoded colors:")
|
||||
for file, line_no, line in files_with_colors[:5]: # Show first 5
|
||||
print(f" {file}:{line_no}: {line[:60]}...")
|
||||
return False
|
||||
else:
|
||||
print(f"✓ No hardcoded colors found in {len(view_files)} view files")
|
||||
return True
|
||||
|
||||
def test_imports():
|
||||
"""Test that all imports work correctly."""
|
||||
print("\n=== Testing Imports ===")
|
||||
|
||||
test_imports = [
|
||||
('themes.theme_config', 'ThemeConfig'),
|
||||
('themes.qss_generator', 'QSSGenerator'),
|
||||
('views.base.theme_aware_widget', 'ThemeAwareWidget'),
|
||||
]
|
||||
|
||||
failed = []
|
||||
|
||||
for module_name, class_name in test_imports:
|
||||
try:
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
if hasattr(module, class_name):
|
||||
print(f"✓ {module_name}.{class_name}")
|
||||
else:
|
||||
print(f"✗ {module_name} missing {class_name}")
|
||||
failed.append(f"{module_name}.{class_name}")
|
||||
except ImportError as e:
|
||||
# Check if it's just PyQt5 missing
|
||||
if 'PyQt5' in str(e):
|
||||
print(f"⚠ {module_name}.{class_name} (requires PyQt5)")
|
||||
else:
|
||||
print(f"✗ {module_name}.{class_name}: {e}")
|
||||
failed.append(f"{module_name}.{class_name}")
|
||||
|
||||
if failed:
|
||||
print(f"\n✗ {len(failed)} imports failed")
|
||||
return False
|
||||
else:
|
||||
print(f"\n✓ All critical imports successful (PyQt5-dependent modules skipped)")
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
print("=" * 60)
|
||||
print("THEME SYSTEM COMPREHENSIVE TEST")
|
||||
print("=" * 60)
|
||||
|
||||
results = []
|
||||
|
||||
# Run tests
|
||||
results.append(("File Structure", test_file_structure()))
|
||||
results.append(("Theme Config", test_theme_config()))
|
||||
results.append(("QSS Generator", test_qss_generator()))
|
||||
results.append(("Imports", test_imports()))
|
||||
results.append(("No Hardcoded Colors", test_no_hardcoded_colors()))
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
passed = sum(1 for _, result in results if result)
|
||||
total = len(results)
|
||||
|
||||
for name, result in results:
|
||||
status = "✓ PASS" if result else "✗ FAIL"
|
||||
print(f"{status}: {name}")
|
||||
|
||||
print(f"\nTotal: {passed}/{total} tests passed")
|
||||
|
||||
if passed == total:
|
||||
print("\n✅ ALL TESTS PASSED! Theme system is properly configured.")
|
||||
else:
|
||||
print(f"\n⚠️ {total - passed} tests failed. Review the output above.")
|
||||
|
||||
return 0 if passed == total else 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren