From bab157e1d25d4455290fbfd8c10d6278928734c9 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Tue, 9 Dec 2025 16:37:00 +1100 Subject: [PATCH] Add configuration management module --- config.py | 400 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 config.py diff --git a/config.py b/config.py new file mode 100644 index 0000000..c084ad2 --- /dev/null +++ b/config.py @@ -0,0 +1,400 @@ +# config.py - Enhanced with folder sequence support +""" +Enhanced Configuration with folder-based sequential playback +""" + +import os +import json +import logging +from pathlib import Path +from datetime import datetime + + +class Config: + """Enhanced configuration class with folder sequence support""" + + # Directory paths + BASE_DIR = Path(__file__).parent + VIDEOS_DIR = BASE_DIR / "videos" + TRAILERS_DIR = VIDEOS_DIR / "trailers" + SPECIFIC_VIDEOS_DIR = VIDEOS_DIR / "specific" + GROUP_VIDEOS_DIR = VIDEOS_DIR / "groupvideos" # New folder for sequences + LOGS_DIR = BASE_DIR / "logs" + + # Mapping files + KEY_MAPPING_FILE = VIDEOS_DIR / "key_mapping.txt" + FOLDER_MAPPING_FILE = VIDEOS_DIR / "folder_mapping.txt" # New mapping for folders + FOLDER_STATE_FILE = VIDEOS_DIR / "folder_state.json" # Track playback position + + # Supported video formats + SUPPORTED_FORMATS = ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'] + + # Playback settings + MIN_PLAYBACK_TIME = 2.0 + TRANSITION_DELAY = 0.5 + SPECIFIC_VIDEO_MIN_PLAY_TIME = 5.0 + + # Video scaling settings + ENABLE_SCALING = True + MAINTAIN_ASPECT_RATIO = True + SCALE_FACTOR = 0 + + # Secondary monitor settings + PREFER_SECONDARY_MONITOR = True + SECONDARY_MONITOR_WIDTH = 1920 + SECONDARY_MONITOR_HEIGHT = 1080 + + # Logging settings + LOG_LEVEL = logging.DEBUG + LOG_FORMAT = '%(asctime)s.%(msecs)03d [%(name)-15s] %(levelname)-8s: %(message)s' + LOG_DATE_FORMAT = '%H:%M:%S' + + # Debug settings + DEBUG_LOG_MAX_LINES = 2000 + DEBUG_UPDATE_INTERVAL = 100 + + # NFC settings + NFC_MIN_LENGTH = 4 + NFC_MAX_LENGTH = 16 + NFC_INPUT_TIMEOUT = 0.5 + + def __init__(self): + """Initialize configuration with folder support""" + self.validate_settings() + self.setup_detailed_logging() + + # Log system information + logger = logging.getLogger(__name__) + logger.info("=== SYSTEM CONFIGURATION ===") + logger.info(f"Base directory: {self.BASE_DIR}") + logger.info(f"Trailers directory: {self.TRAILERS_DIR}") + logger.info(f"Specific videos directory: {self.SPECIFIC_VIDEOS_DIR}") + logger.info(f"Group videos directory: {self.GROUP_VIDEOS_DIR}") + logger.info(f"Supported formats: {self.SUPPORTED_FORMATS}") + logger.info("=== CONFIGURATION COMPLETE ===") + + def validate_settings(self): + """Validate and create directories""" + directories = [ + self.VIDEOS_DIR, + self.TRAILERS_DIR, + self.SPECIFIC_VIDEOS_DIR, + self.GROUP_VIDEOS_DIR, + self.LOGS_DIR + ] + + for directory in directories: + directory.mkdir(exist_ok=True, parents=True) + + def setup_detailed_logging(self): + """Setup comprehensive logging system""" + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_file = self.LOGS_DIR / f"video_player_{timestamp}.log" + + # Clear any existing handlers + root_logger = logging.getLogger() + if root_logger.handlers: + for handler in root_logger.handlers: + root_logger.removeHandler(handler) + + # Create file handler with explicit settings + file_handler = logging.FileHandler(log_file, encoding='utf-8', mode='w') + file_handler.setLevel(self.LOG_LEVEL) + file_handler.setFormatter(logging.Formatter( + fmt=self.LOG_FORMAT, + datefmt=self.LOG_DATE_FORMAT + )) + + # Create console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(self.LOG_LEVEL) + console_handler.setFormatter(logging.Formatter( + fmt=self.LOG_FORMAT, + datefmt=self.LOG_DATE_FORMAT + )) + + # Configure root logger + root_logger.setLevel(self.LOG_LEVEL) + root_logger.addHandler(file_handler) + root_logger.addHandler(console_handler) + + # Reduce noise from VLC + logging.getLogger('vlc').setLevel(logging.WARNING) + logging.getLogger('mpv').setLevel(logging.WARNING) + + logger = logging.getLogger(__name__) + logger.info(f"Logging initialized - Log file: {log_file}") + logger.info(f"Log file location: {log_file.absolute()}") + + def load_key_mapping(self): + """Load NFC mappings for single files (legacy support)""" + logger = logging.getLogger(__name__) + key_map = {} + + try: + if not self.KEY_MAPPING_FILE.exists(): + logger.warning(f"Key mapping file not found: {self.KEY_MAPPING_FILE}") + self.create_example_mapping() + return {} + + logger.info(f"Loading key mappings from: {self.KEY_MAPPING_FILE}") + + with open(self.KEY_MAPPING_FILE, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + line = line.strip() + + if not line or line.startswith('#'): + continue + + if ',' not in line: + continue + + parts = line.split(',', 1) + if len(parts) != 2: + continue + + nfc_id = parts[0].strip() + movie_name = parts[1].strip() + + if not nfc_id or not movie_name: + continue + + found_file = self.find_video_file(movie_name) + + if found_file: + key_map[nfc_id] = str(found_file) + logger.debug(f"Mapped NFC {nfc_id} -> {found_file.name}") + else: + key_map[nfc_id] = f"Missing: {movie_name}" + logger.warning(f"Video file not found for '{movie_name}' (NFC: {nfc_id})") + + logger.info(f"Loaded {len(key_map)} key mappings") + return key_map + + except Exception as e: + logger.error(f"Error loading key mapping: {e}", exc_info=True) + return {} + + def load_folder_mapping(self): + """Load NFC mappings for folder sequences""" + logger = logging.getLogger(__name__) + folder_map = {} + + try: + if not self.FOLDER_MAPPING_FILE.exists(): + logger.info("Creating example folder mapping file") + self.create_example_folder_mapping() + return {} + + logger.info(f"Loading folder mappings from: {self.FOLDER_MAPPING_FILE}") + + with open(self.FOLDER_MAPPING_FILE, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + line = line.strip() + + if not line or line.startswith('#'): + continue + + if ',' not in line: + continue + + parts = line.split(',', 1) + if len(parts) != 2: + continue + + nfc_id = parts[0].strip() + folder_name = parts[1].strip() + + if not nfc_id or not folder_name: + continue + + # Check if folder exists + folder_path = self.GROUP_VIDEOS_DIR / folder_name + if folder_path.exists() and folder_path.is_dir(): + # Get all videos in folder, sorted + videos = self.get_sorted_videos_in_folder(folder_path) + if videos: + folder_map[nfc_id] = { + 'folder_name': folder_name, + 'folder_path': str(folder_path), + 'videos': videos + } + logger.info(f"Mapped NFC {nfc_id} -> Folder '{folder_name}' ({len(videos)} videos)") + else: + logger.warning(f"No videos found in folder '{folder_name}' (NFC: {nfc_id})") + else: + logger.warning(f"Folder not found: '{folder_name}' (NFC: {nfc_id})") + + logger.info(f"Loaded {len(folder_map)} folder mappings") + return folder_map + + except Exception as e: + logger.error(f"Error loading folder mapping: {e}", exc_info=True) + return {} + + def get_sorted_videos_in_folder(self, folder_path): + """Get all videos in a folder, sorted naturally""" + videos = [] + + try: + for file_path in sorted(folder_path.iterdir()): + if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_FORMATS: + videos.append(str(file_path)) + except Exception as e: + logging.getLogger(__name__).error(f"Error reading folder {folder_path}: {e}") + + return videos + + def load_folder_state(self): + """Load the current playback position for each folder""" + logger = logging.getLogger(__name__) + + try: + if self.FOLDER_STATE_FILE.exists(): + with open(self.FOLDER_STATE_FILE, 'r', encoding='utf-8') as f: + state = json.load(f) + logger.info(f"Loaded folder state: {len(state)} folders tracked") + return state + else: + logger.info("No folder state file found - starting fresh") + return {} + except Exception as e: + logger.error(f"Error loading folder state: {e}") + return {} + + def save_folder_state(self, state): + """Save the current playback position for each folder""" + logger = logging.getLogger(__name__) + + try: + with open(self.FOLDER_STATE_FILE, 'w', encoding='utf-8') as f: + json.dump(state, f, indent=2) + logger.debug(f"Saved folder state: {len(state)} folders") + except Exception as e: + logger.error(f"Error saving folder state: {e}") + + def reset_folder_state(self): + """Reset all folder playback positions to the beginning""" + logger = logging.getLogger(__name__) + + try: + if self.FOLDER_STATE_FILE.exists(): + self.FOLDER_STATE_FILE.unlink() + logger.info("Folder state reset - all sequences will start from beginning") + return True + except Exception as e: + logger.error(f"Error resetting folder state: {e}") + return False + + def find_video_file(self, movie_name): + """Enhanced video file matching""" + for file_path in self.SPECIFIC_VIDEOS_DIR.iterdir(): + if (file_path.is_file() and + file_path.suffix.lower() in self.SUPPORTED_FORMATS and + file_path.stem.lower() == movie_name.lower()): + return file_path + + for file_path in self.SPECIFIC_VIDEOS_DIR.iterdir(): + if (file_path.is_file() and + file_path.suffix.lower() in self.SUPPORTED_FORMATS and + movie_name.lower() in file_path.stem.lower()): + return file_path + + return None + + def create_example_mapping(self): + """Create example key mapping file for single videos""" + try: + example_content = """# NFC Key Mapping File for Single Video Files +# Format: NFC_ID, Movie_Name +# These map to individual files in the 'specific' folder +# +# Examples: +# 12345678,avengers +# 87654321,spider-man +# +# For FOLDER sequences, use folder_mapping.txt instead! +""" + + with open(self.KEY_MAPPING_FILE, 'w', encoding='utf-8') as f: + f.write(example_content) + + except Exception as e: + logging.getLogger(__name__).error(f"Failed to create example mapping: {e}") + + def create_example_folder_mapping(self): + """Create example folder mapping file""" + logger = logging.getLogger(__name__) + + try: + example_content = """# NFC Folder Mapping File for Sequential Video Playback +# Format: NFC_ID, Folder_Name +# +# Instructions: +# 1. Each line maps an NFC tag to a FOLDER of videos +# 2. Videos in the folder will play in sequential order +# 3. Each time you scan the tag, it plays the NEXT video in sequence +# 4. Folders must be in: videos/groupvideos/ +# +# Example folder structure: +# videos/groupvideos/LOTR/video1.mp4 +# videos/groupvideos/LOTR/video2.mp4 +# videos/groupvideos/LOTR/video3.mp4 +# videos/groupvideos/Avengers/video1.mp4 +# videos/groupvideos/Avengers/video2.mp4 +# +# Example mappings: +# 11111111,LOTR +# 22222222,Avengers +# 33333333,Jurassic +# 44444444,StarWars +# +# Tips: +# - Folder names are case-sensitive +# - Videos play in alphabetical order (name your files accordingly) +# - Use numbers/prefixes to control order: 01_intro.mp4, 02_main.mp4, etc. +# - Each scan advances to the next video, then loops back to start +# +# Your folder mappings below: +""" + + with open(self.FOLDER_MAPPING_FILE, 'w', encoding='utf-8') as f: + f.write(example_content) + + logger.info(f"Created example folder mapping file: {self.FOLDER_MAPPING_FILE}") + + # Create example folder structure + example_folders = ['LOTR', 'Avengers', 'Jurassic'] + for folder_name in example_folders: + folder_path = self.GROUP_VIDEOS_DIR / folder_name + folder_path.mkdir(exist_ok=True) + logger.info(f"Created example folder: {folder_path}") + + except Exception as e: + logger.error(f"Failed to create example folder mapping: {e}") + + def get_video_files(self, directory: Path): + """Get video files with enhanced logging""" + logger = logging.getLogger(__name__) + + if not directory.exists(): + logger.warning(f"Directory does not exist: {directory}") + return [] + + video_files = [] + for file_path in directory.iterdir(): + if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_FORMATS: + video_files.append(file_path) + + return video_files + + def get_display_settings(self): + """Get display settings for video window""" + return { + 'prefer_secondary': self.PREFER_SECONDARY_MONITOR, + 'secondary_width': self.SECONDARY_MONITOR_WIDTH, + 'secondary_height': self.SECONDARY_MONITOR_HEIGHT, + 'enable_scaling': self.ENABLE_SCALING, + 'maintain_aspect': self.MAINTAIN_ASPECT_RATIO, + 'scale_factor': self.SCALE_FACTOR + }