# logger.py import os import threading import sys import logging # <-- Import the logging module # --- Constants --- APP_LOG_FILE = "rentry_downloader.log" # --- Global Logger Instance --- # Get the root logger for the application app_logger = logging.getLogger("RentryDownloader") # Prevent propagation to avoid duplicate messages if root logger is configured elsewhere app_logger.propagate = False # Create a lock for file operations (still needed for success/fail logs) _log_lock = threading.Lock() # --- Debugging (Existing) --- _debug_enabled = False # Global flag for debug status # --- MODIFIED: setup_logging --- def setup_logging(config): """Sets up both file logging and the debug flag.""" global _debug_enabled, app_logger # --- File Logging Setup --- log_level_str = config.get("Misc", "log_level", fallback="INFO").upper() try: log_level = getattr(logging, log_level_str, logging.INFO) except AttributeError: print( f"[Logger] Warning: Invalid log_level '{log_level_str}' in config. Using INFO.", file=sys.stderr, ) log_level = logging.INFO # Set the overall level for the logger app_logger.setLevel(log_level) # Prevent adding handlers multiple times if called again if not app_logger.handlers: # Create File Handler try: file_handler = logging.FileHandler( APP_LOG_FILE, mode="a", encoding="utf-8" ) # Append mode file_handler.setLevel(log_level) # Create Formatter formatter = logging.Formatter( "%(asctime)s - %(levelname)s - [%(threadName)s] - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) file_handler.setFormatter(formatter) # Add Handler to Logger app_logger.addHandler(file_handler) print( f"[Logger] Application logging configured. Level: {log_level_str}. File: '{APP_LOG_FILE}'" ) app_logger.info("--- Logging Initialized ---") except Exception as e: print( f"[Logger] CRITICAL ERROR: Failed to configure file logging to '{APP_LOG_FILE}': {e}", file=sys.stderr, ) # Optionally, add a StreamHandler as a fallback if file logging fails if not any(isinstance(h, logging.StreamHandler) for h in app_logger.handlers): stream_handler = logging.StreamHandler(sys.stderr) stream_handler.setLevel(logging.WARNING) # Log warnings and above to stderr formatter = logging.Formatter( "%(asctime)s - %(levelname)s - [%(threadName)s] - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) stream_handler.setFormatter(formatter) app_logger.addHandler(stream_handler) print("[Logger] Fallback stderr logging enabled for WARNING+ levels.", file=sys.stderr) # --- Debug Mode Setup (Existing) --- try: _debug_enabled = config.getboolean("Misc", "debug_mode", fallback=False) if _debug_enabled: # If debug mode is on, ensure the logger level is at least DEBUG if log_level > logging.DEBUG: app_logger.setLevel(logging.DEBUG) # Also set handler level if it was higher for handler in app_logger.handlers: if handler.level > logging.DEBUG: handler.setLevel(logging.DEBUG) print("[Logger] Debug mode enabled via config. Logger level set to DEBUG.", file=sys.stderr) else: print("[Logger] Debug mode enabled via config.", file=sys.stderr) # Add a specific handler for debug messages to stderr if not already present # This keeps debug separate from the main file log if desired, or duplicates if level is DEBUG # Let's keep the existing debug_print logic for stderr for now to minimize changes. # If you want debug messages *only* in the file when debug_mode=true, adjust levels above. except Exception as e: print( f"[Logger] Warning: Could not read debug_mode from config: {e}", file=sys.stderr, ) _debug_enabled = False # --- Debug Print (Existing - outputs to stderr) --- def debug_print(*args, **kwargs): """Prints messages to stderr only if debug mode is enabled.""" if _debug_enabled: message = " ".join(map(str, args)) print(f"[DEBUG] {message}", file=sys.stderr, **kwargs) # --- Log File Loading/Manipulation (Existing - for success/fail logs) --- # These functions remain unchanged as they handle the specific success/failure *data* files # They use _log_lock for thread safety on these specific files. def load_log_file(filepath, is_success_log=False): """ Loads lines from a log file into a set. If is_success_log is True, it strips size info before adding to the set. """ entries = set() if not os.path.exists(filepath): debug_print(f"Log file not found: {filepath}") return entries debug_print(f"Loading log file: {filepath} (Success Log Mode: {is_success_log})") try: # Reading is generally safe, but lock for consistency if needed # with _log_lock: with open(filepath, "r", encoding="utf-8") as f: for i, line in enumerate(f): entry = line.strip() if not entry: continue original_entry = entry # Keep original for debug if is_success_log: entry = entry.split(" | Size:")[0].strip() if entry: entries.add(entry) # debug_print(f" Loaded entry {i+1}: '{entry}' (from '{original_entry}')") debug_print( f"Finished loading {len(entries)} unique entries from {filepath}" ) except Exception as e: # Use app_logger for warnings about these files too app_logger.warning(f"Could not read log file '{filepath}': {e}") print(f"Warning: Could not read log file '{filepath}': {e}", file=sys.stderr) # Keep stderr print for visibility return entries def append_log_file(filepath, entry): """Appends a single entry to a log file (thread-safe).""" debug_print(f"Attempting to append to log: {filepath}, Entry: '{entry}'") try: log_dir = os.path.dirname(filepath) with _log_lock: # Use the lock for these specific files if log_dir and not os.path.exists(log_dir): debug_print(f"Creating log directory: {log_dir}") os.makedirs(log_dir) with open(filepath, "a", encoding="utf-8") as f: f.write(f"{entry}\n") debug_print(f"Successfully appended to log: {filepath}") except Exception as e: thread_name = threading.current_thread().name # Log error using app_logger app_logger.error(f"Error appending to log file '{filepath}': {e}") print( f"[{thread_name}] Error appending to log file '{filepath}': {e}", file=sys.stderr, ) # Keep stderr print def remove_from_log_file(filepath, entry_to_remove, is_success_log=False): """ Removes a specific entry from a log file (thread-safe). If is_success_log is True, it matches based on filename only (ignoring size). """ lines = set() rewritten = False entry_found = False thread_name = threading.current_thread().name debug_print( f"Attempting to remove from log: {filepath}, Entry: '{entry_to_remove}', Success Mode: {is_success_log}" ) try: with _log_lock: # Use the lock for these specific files if os.path.exists(filepath): debug_print(f"Reading existing log for removal: {filepath}") with open(filepath, "r", encoding="utf-8") as f_read: for line in f_read: current_entry = line.strip() if not current_entry: continue compare_part = current_entry if is_success_log: compare_part = current_entry.split(" | Size:")[0].strip() if compare_part == entry_to_remove: entry_found = True rewritten = True debug_print( f" Match found, removing line: '{current_entry}'" ) else: lines.add(current_entry) # Keep this line else: debug_print(f"Log file not found, nothing to remove: {filepath}") return False if rewritten: debug_print(f"Rewriting log file: {filepath}") with open(filepath, "w", encoding="utf-8") as f_write: for line in sorted(list(lines)): f_write.write(f"{line}\n") debug_print(f"Finished rewriting log: {filepath}") return True else: debug_print( f"Entry '{entry_to_remove}' not found in {filepath}, no rewrite needed." ) return False # Return False if not found except Exception as e: # Log error using app_logger app_logger.error(f"Error during log removal in '{filepath}': {e}") print( f"[{thread_name}] Error during log removal in '{filepath}': {e}", file=sys.stderr, ) # Keep stderr print return False if __name__ == "__main__": # Simple config mock for testing class MockConfig: def get(self, section, option, fallback=None): if section == "Misc" and option == "log_level": return "DEBUG" return fallback def getboolean(self, section, option, fallback=False): if section == "Misc" and option == "debug_mode": return True return fallback print("--- Testing logger.py ---") # Setup logging using the mock config setup_logging(MockConfig()) # Test the application logger app_logger.debug("This is a debug message for the app log.") app_logger.info("This is an info message for the app log.") app_logger.warning("This is a warning message.") app_logger.error("This is an error message.") app_logger.critical("This is a critical message.") # Test debug_print (should go to stderr) print("\nTesting debug_print (should appear on stderr if debug enabled):") debug_print("This is a debug_print message.") # Test success/fail log functions (remain mostly unchanged) print("\nTesting success/fail log functions:") test_success_log = "test_success_logger.txt" test_fail_log = "test_fail_logger.txt" if os.path.exists(test_success_log): os.remove(test_success_log) if os.path.exists(test_fail_log): os.remove(test_fail_log) append_log_file(test_success_log, "file1.zip | Size: 1.10 MB") append_log_file(test_fail_log, "Name: Item 2 | URL: http://example.com/2") print(f"Success log contents: {load_log_file(test_success_log)}") print(f"Failure log contents: {load_log_file(test_fail_log)}") remove_from_log_file(test_success_log, "file1.zip", is_success_log=True) print(f"Success log after removal: {load_log_file(test_success_log)}") if os.path.exists(test_success_log): os.remove(test_success_log) if os.path.exists(test_fail_log): os.remove(test_fail_log) if os.path.exists(APP_LOG_FILE): print(f"\nCheck the contents of '{APP_LOG_FILE}' for application logs.") print("\n--- Test complete ---")