diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..a329c3d --- /dev/null +++ b/src/README.md @@ -0,0 +1,111 @@ +# Signage Player - Complete Offline Installation Package + +This directory contains everything needed to install the Signage Player application completely offline on a Raspberry Pi or similar Debian-based system. + +## Directory Structure + +``` +src/ +├── offline_packages/ # Pre-downloaded Python packages (.whl files) +│ ├── requests-2.32.4-py3-none-any.whl +│ ├── pillow-11.1.0-cp311-cp311-linux_armv7l.whl +│ ├── pygame-2.6.1-cp311-cp311-linux_armv7l.whl +│ └── ... (dependencies) +├── shared_modules/ # Shared Python modules +│ ├── logging_config.py +│ └── python_functions.py +├── system_packages/ # System dependency information +│ └── apt_packages.txt # List of required APT packages +└── scripts/ # Installation and utility scripts + ├── install_offline.sh # Main offline installation script + └── check_dependencies.sh # Dependency verification script +``` + +## Quick Installation + +1. **Run the offline installer:** + ```bash + cd /path/to/signage-player + chmod +x src/scripts/install_offline.sh + ./src/scripts/install_offline.sh + ``` + +2. **Verify installation:** + ```bash + chmod +x src/scripts/check_dependencies.sh + ./src/scripts/check_dependencies.sh + ``` + +3. **Run the application:** + ```bash + ./run_tkinter_debug.sh + ``` + +## What Gets Installed + +### System Packages (via APT) +- Python 3 development tools +- OpenCV libraries and Python bindings +- SDL2 libraries for pygame +- Image processing libraries (JPEG, PNG, TIFF, WebP) +- Audio libraries +- Build tools + +### Python Packages (from offline wheels) +- **requests** - HTTP library for server communication +- **pillow** - Image processing library +- **pygame** - Audio and input handling +- **certifi, charset_normalizer, idna, urllib3** - Dependencies + +### Application Components +- Shared logging and playlist management modules +- Modern tkinter-based media player +- Configuration management +- Resource directories + +## Manual Installation Steps + +If you prefer to install manually: + +1. **Install system packages:** + ```bash + sudo apt update + cat src/system_packages/apt_packages.txt | grep -v '^#' | xargs sudo apt install -y + ``` + +2. **Create virtual environment:** + ```bash + python3 -m venv venv + source venv/bin/activate + ``` + +3. **Install Python packages:** + ```bash + pip install --no-index --find-links src/offline_packages requests pillow pygame + ``` + +4. **Copy shared modules:** + ```bash + cp src/shared_modules/*.py tkinter_app/src/ + ``` + +## Troubleshooting + +- **Permission errors:** Make sure scripts are executable with `chmod +x` +- **Missing packages:** Run `src/scripts/check_dependencies.sh` to verify installation +- **Virtual environment issues:** Delete `venv` folder and re-run installer +- **OpenCV errors:** Ensure `python3-opencv` system package is installed + +## Requirements + +- Debian/Ubuntu-based system (Raspberry Pi OS recommended) +- Internet connection for system package installation (APT only) +- Sudo privileges for system package installation +- At least 200MB free space + +## Notes + +- This package includes all Python dependencies as pre-compiled wheels +- No internet connection needed for Python packages during installation +- Compatible with ARM-based systems (Raspberry Pi) +- Includes fallback mechanisms for offline operation \ No newline at end of file diff --git a/src/offline_packages/certifi-2025.8.3-py3-none-any.whl b/src/offline_packages/certifi-2025.8.3-py3-none-any.whl new file mode 100644 index 0000000..b4158ec Binary files /dev/null and b/src/offline_packages/certifi-2025.8.3-py3-none-any.whl differ diff --git a/src/offline_packages/charset_normalizer-3.4.2-py3-none-any.whl b/src/offline_packages/charset_normalizer-3.4.2-py3-none-any.whl new file mode 100644 index 0000000..61a1d21 Binary files /dev/null and b/src/offline_packages/charset_normalizer-3.4.2-py3-none-any.whl differ diff --git a/src/offline_packages/idna-3.10-py3-none-any.whl b/src/offline_packages/idna-3.10-py3-none-any.whl new file mode 100644 index 0000000..52759bd Binary files /dev/null and b/src/offline_packages/idna-3.10-py3-none-any.whl differ diff --git a/src/offline_packages/pillow-11.1.0-cp311-cp311-linux_armv7l.whl b/src/offline_packages/pillow-11.1.0-cp311-cp311-linux_armv7l.whl new file mode 100644 index 0000000..29187f6 Binary files /dev/null and b/src/offline_packages/pillow-11.1.0-cp311-cp311-linux_armv7l.whl differ diff --git a/src/offline_packages/pygame-2.6.1-cp311-cp311-linux_armv7l.whl b/src/offline_packages/pygame-2.6.1-cp311-cp311-linux_armv7l.whl new file mode 100644 index 0000000..2e38b05 Binary files /dev/null and b/src/offline_packages/pygame-2.6.1-cp311-cp311-linux_armv7l.whl differ diff --git a/src/offline_packages/requests-2.32.4-py3-none-any.whl b/src/offline_packages/requests-2.32.4-py3-none-any.whl new file mode 100644 index 0000000..d52fad0 Binary files /dev/null and b/src/offline_packages/requests-2.32.4-py3-none-any.whl differ diff --git a/src/offline_packages/urllib3-2.5.0-py3-none-any.whl b/src/offline_packages/urllib3-2.5.0-py3-none-any.whl new file mode 100644 index 0000000..81b580f Binary files /dev/null and b/src/offline_packages/urllib3-2.5.0-py3-none-any.whl differ diff --git a/src/scripts/check_dependencies.sh b/src/scripts/check_dependencies.sh new file mode 100755 index 0000000..cb25c54 --- /dev/null +++ b/src/scripts/check_dependencies.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# Dependency Verification Script +# Checks if all required dependencies are properly installed + +echo "=== Signage Player Dependency Check ===" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +ERRORS=0 + +# Function to check system package +check_system_package() { + if dpkg -l | grep -q "^ii.*$1"; then + echo -e "${GREEN}✓${NC} $1 is installed" + else + echo -e "${RED}✗${NC} $1 is NOT installed" + ((ERRORS++)) + fi +} + +# Function to check Python package +check_python_package() { + if python3 -c "import $1" 2>/dev/null; then + echo -e "${GREEN}✓${NC} Python package '$1' is available" + else + echo -e "${RED}✗${NC} Python package '$1' is NOT available" + ((ERRORS++)) + fi +} + +echo "Checking system packages..." +check_system_package "python3-dev" +check_system_package "python3-opencv" +check_system_package "libsdl2-dev" +check_system_package "libjpeg-dev" + +echo "" +echo "Checking Python packages..." +check_python_package "cv2" +check_python_package "pygame" +check_python_package "PIL" +check_python_package "requests" + +echo "" +echo "Checking application files..." +if [ -f "tkinter_app/src/main.py" ]; then + echo -e "${GREEN}✓${NC} Main application file exists" +else + echo -e "${RED}✗${NC} Main application file missing" + ((ERRORS++)) +fi + +if [ -f "tkinter_app/src/tkinter_simple_player.py" ]; then + echo -e "${GREEN}✓${NC} Player module exists" +else + echo -e "${RED}✗${NC} Player module missing" + ((ERRORS++)) +fi + +if [ -d "venv" ]; then + echo -e "${GREEN}✓${NC} Virtual environment exists" +else + echo -e "${YELLOW}⚠${NC} Virtual environment not found" +fi + +echo "" +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}=== All dependencies are properly installed! ===${NC}" + echo "You can run the application with: ./run_tkinter_debug.sh" +else + echo -e "${RED}=== Found $ERRORS issues ===${NC}" + echo "Run the installation script: src/scripts/install_offline.sh" +fi \ No newline at end of file diff --git a/src/scripts/install_offline.sh b/src/scripts/install_offline.sh new file mode 100755 index 0000000..abe4e2c --- /dev/null +++ b/src/scripts/install_offline.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# Offline Installation Script for Signage Player +# This script installs all dependencies and sets up the application completely offline + +set -e # Exit on any error + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +echo "=== Signage Player Offline Installation ===" +echo "Project root: $PROJECT_ROOT" + +# Check if running as root for system packages +if [[ $EUID -eq 0 ]]; then + echo "ERROR: Please run this script as a regular user, not as root." + echo "The script will prompt for sudo when needed." + exit 1 +fi + +# Function to check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Install system packages +echo "Step 1: Installing system packages..." +if command_exists apt; then + echo "Installing APT packages..." + sudo apt update + + # Read and install packages from the list + while IFS= read -r package; do + # Skip comments and empty lines + if [[ ! "$package" =~ ^[[:space:]]*# ]] && [[ -n "${package// }" ]]; then + echo "Installing: $package" + sudo apt install -y "$package" || echo "Warning: Could not install $package" + fi + done < "$SCRIPT_DIR/../system_packages/apt_packages.txt" +else + echo "ERROR: apt package manager not found. This script is designed for Debian/Ubuntu systems." + exit 1 +fi + +# Create virtual environment +echo "Step 2: Creating virtual environment..." +cd "$PROJECT_ROOT" +if [ ! -d "venv" ]; then + python3 -m venv venv + echo "Virtual environment created." +else + echo "Virtual environment already exists." +fi + +# Activate virtual environment +echo "Step 3: Activating virtual environment..." +source venv/bin/activate + +# Install Python packages from offline wheels +echo "Step 4: Installing Python packages from offline wheels..." +if [ -d "src/offline_packages" ]; then + pip install --upgrade pip + pip install --no-index --find-links "src/offline_packages" \ + requests pillow pygame certifi charset_normalizer idna urllib3 + echo "Python packages installed successfully." +else + echo "ERROR: Offline packages directory not found!" + exit 1 +fi + +# Copy shared modules to tkinter app +echo "Step 5: Setting up shared modules..." +if [ -d "src/shared_modules" ]; then + cp src/shared_modules/*.py tkinter_app/src/ 2>/dev/null || echo "Shared modules already in place." + echo "Shared modules configured." +else + echo "Warning: Shared modules directory not found." +fi + +# Create necessary directories +echo "Step 6: Creating application directories..." +mkdir -p tkinter_app/resources/static/resurse +mkdir -p tkinter_app/src/static/resurse + +# Set permissions +echo "Step 7: Setting permissions..." +chmod +x run_tkinter_debug.sh +chmod +x install_tkinter.sh +chmod +x src/scripts/*.sh 2>/dev/null || true + +echo "" +echo "=== Installation Complete! ===" +echo "" +echo "To run the signage player:" +echo " ./run_tkinter_debug.sh" +echo "" +echo "To configure settings, run the app and click the Settings button." +echo "" +echo "The application has been set up with:" +echo " - All system dependencies" +echo " - Python virtual environment with required packages" +echo " - Shared modules properly configured" +echo " - Necessary directories created" +echo "" \ No newline at end of file diff --git a/src/shared_modules/logging_config.py b/src/shared_modules/logging_config.py new file mode 100644 index 0000000..8710312 --- /dev/null +++ b/src/shared_modules/logging_config.py @@ -0,0 +1,27 @@ +import logging +import os + +# Path to the log file +# Update the path to point to the new resources directory +LOG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt') + +# Create a logger instance +Logger = logging.getLogger('SignageApp') +Logger.setLevel(logging.INFO) # Set the logging level to INFO + +# Create a file handler to write logs to the log.txt file +file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a') # Append logs to the file +file_handler.setLevel(logging.INFO) + +# Create a formatter for the log messages +formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s') +file_handler.setFormatter(formatter) + +# Add the file handler to the logger +Logger.addHandler(file_handler) + +# Optionally, add a stream handler to log messages to the console +stream_handler = logging.StreamHandler() +stream_handler.setLevel(logging.INFO) +stream_handler.setFormatter(formatter) +Logger.addHandler(stream_handler) \ No newline at end of file diff --git a/src/shared_modules/python_functions.py b/src/shared_modules/python_functions.py new file mode 100644 index 0000000..279b46e --- /dev/null +++ b/src/shared_modules/python_functions.py @@ -0,0 +1,196 @@ +import os +import json +import requests +from logging_config import Logger # Import the shared logger +import bcrypt +import time + +# Update paths to use the new directory structure +CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt') +LOCAL_PLAYLIST_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'local_playlist.json') + +def load_config(): + """Load configuration from app_config.txt.""" + Logger.info("python_functions: Starting load_config function.") + if os.path.exists(CONFIG_FILE): + try: + with open(CONFIG_FILE, 'r') as file: + Logger.info("python_functions: Configuration file loaded successfully.") + return json.load(file) + except json.JSONDecodeError as e: + Logger.error(f"python_functions: Failed to parse configuration file. Error: {e}") + return {} + else: + Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.") + return {} + Logger.info("python_functions: Finished load_config function.") + +# Load configuration and initialize variables +config_data = load_config() +server = config_data.get("server_ip", "") +host = config_data.get("screen_name", "") +quick = config_data.get("quickconnect_key", "") +port = config_data.get("port", "") + +Logger.info(f"python_functions: Configuration loaded: server={server}, host={host}, quick={quick}, port={port}") + +def load_local_playlist(): + """Load the playlist and version from local storage.""" + Logger.info("python_functions: Starting load_local_playlist function.") + if os.path.exists(LOCAL_PLAYLIST_FILE): + try: + with open(LOCAL_PLAYLIST_FILE, 'r') as local_file: + local_playlist = json.load(local_file) + Logger.info(f"python_functions: Local playlist loaded: {local_playlist}") + if isinstance(local_playlist, dict) and 'playlist' in local_playlist and 'version' in local_playlist: + Logger.info("python_functions: Finished load_local_playlist function successfully.") + return local_playlist # Return the full playlist data + else: + Logger.error("python_functions: Invalid local playlist structure.") + return {'playlist': [], 'version': 0} + except json.JSONDecodeError as e: + Logger.error(f"python_functions: Failed to parse local playlist file. Error: {e}") + return {'playlist': [], 'version': 0} + else: + Logger.warning("python_functions: Local playlist file not found.") + return {'playlist': [], 'version': 0} + Logger.info("python_functions: Finished load_local_playlist function.") + +def save_local_playlist(playlist): + """Save the updated playlist locally.""" + Logger.info("python_functions: Starting save_local_playlist function.") + Logger.debug(f"python_functions: Playlist to save: {playlist}") + if not playlist or 'playlist' not in playlist: + Logger.error("python_functions: Invalid playlist data. Cannot save local playlist.") + return + + try: + with open(LOCAL_PLAYLIST_FILE, 'w') as local_file: + json.dump(playlist, local_file, indent=4) # Ensure proper formatting + Logger.info("python_functions: Updated local playlist with server data.") + except IOError as e: + Logger.error(f"python_functions: Failed to save local playlist: {e}") + Logger.info("python_functions: Finished save_local_playlist function.") + +def fetch_server_playlist(): + """Fetch the updated playlist from the server.""" + try: + server_ip = f'{server}:{port}' # Construct the server IP with port + url = f'http://{server_ip}/api/playlists' + params = { + 'hostname': host, + 'quickconnect_code': quick + } + Logger.info(f"Fetching playlist from URL: {url} with params: {params}") + response = requests.get(url, params=params) + + if response.status_code == 200: + response_data = response.json() + Logger.info(f"Server response: {response_data}") + playlist = response_data.get('playlist', []) + version = response_data.get('playlist_version', None) + hashed_quickconnect = response_data.get('hashed_quickconnect', None) + + if version is not None and hashed_quickconnect is not None: + if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')): + Logger.info("Fetched updated playlist from server.") + + # Update the playlist version in app_config.txt + update_config_playlist_version(version) + + return {'playlist': playlist, 'version': version} + else: + Logger.error("Quickconnect code validation failed.") + else: + Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.") + else: + Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}") + except requests.exceptions.RequestException as e: + Logger.error(f"Failed to fetch playlist: {e}") + + return {'playlist': [], 'version': 0} + +def download_media_files(playlist, version): + """Download media files from the server and update the local playlist.""" + Logger.info("python_functions: Starting media file download...") + base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') # Path to the local folder + if not os.path.exists(base_dir): + os.makedirs(base_dir) + Logger.info(f"python_functions: Created directory {base_dir} for media files.") + + updated_playlist = [] # List to store updated media entries + + for media in playlist: + file_name = media.get('file_name', '') + file_url = media.get('url', '') + duration = media.get('duration', 10) # Default duration if not provided + local_path = os.path.join(base_dir, file_name) # Local file path + + Logger.debug(f"python_functions: Preparing to download {file_name} from {file_url}...") + + if os.path.exists(local_path): + Logger.info(f"python_functions: File {file_name} already exists. Skipping download.") + else: + try: + response = requests.get(file_url, timeout=10) + if response.status_code == 200: + with open(local_path, 'wb') as file: + file.write(response.content) + Logger.info(f"python_functions: Successfully downloaded {file_name} to {local_path}") + else: + Logger.error(f"python_functions: Failed to download {file_name}. Status Code: {response.status_code}") + continue + except requests.exceptions.RequestException as e: + Logger.error(f"python_functions: Error downloading {file_name}: {e}") + continue + + # Update the playlist entry to point to the local file path + updated_media = { + 'file_name': file_name, + 'url': f"static/resurse/{file_name}", # Update URL to local path + 'duration': duration + } + Logger.debug(f"python_functions: Updated media entry: {updated_media}") + updated_playlist.append(updated_media) + + # Save the updated playlist locally + save_local_playlist({'playlist': updated_playlist, 'version': version}) + Logger.info("python_functions: Finished media file download and updated local playlist.") + +def clean_unused_files(playlist): + """Remove unused media files from the resource folder.""" + Logger.info("python_functions: Cleaning unused media files...") + base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') + if not os.path.exists(base_dir): + Logger.debug(f"python_functions: Directory {base_dir} does not exist. No files to clean.") + return + + playlist_files = {media.get('file_name', '') for media in playlist} + all_files = set(os.listdir(base_dir)) + unused_files = all_files - playlist_files + + for file_name in unused_files: + file_path = os.path.join(base_dir, file_name) + try: + os.remove(file_path) + Logger.info(f"python_functions: Deleted unused file: {file_path}") + except OSError as e: + Logger.error(f"python_functions: Failed to delete {file_path}: {e}") + +def update_config_playlist_version(version): + """Update the playlist version in app_config.txt.""" + if not os.path.exists(CONFIG_FILE): + Logger.error(f"python_functions: Configuration file {CONFIG_FILE} not found.") + return + + try: + with open(CONFIG_FILE, 'r') as file: + config_data = json.load(file) + + config_data['playlist_version'] = version # Add or update the playlist version + + with open(CONFIG_FILE, 'w') as file: + json.dump(config_data, file, indent=4) + Logger.info(f"python_functions: Updated playlist version in app_config.txt to {version}.") + except (IOError, json.JSONDecodeError) as e: + Logger.error(f"python_functions: Failed to update playlist version in app_config.txt. Error: {e}") \ No newline at end of file diff --git a/src/system_packages/apt_packages.txt b/src/system_packages/apt_packages.txt new file mode 100644 index 0000000..162f745 --- /dev/null +++ b/src/system_packages/apt_packages.txt @@ -0,0 +1,43 @@ +#!/bin/bash +# System Package Dependencies for Signage Player +# These packages need to be installed via apt before installing Python packages + +# Core system packages +python3-dev +python3-pip +python3-venv +python3-setuptools +python3-wheel + +# OpenCV system dependencies +libopencv-dev +python3-opencv +libopencv-core-dev +libopencv-imgproc-dev +libopencv-imgcodecs-dev +libopencv-videoio-dev + +# Audio/Video libraries +libasound2-dev +libsdl2-dev +libsdl2-image-dev +libsdl2-mixer-dev +libsdl2-ttf-dev +libfreetype6-dev +libportmidi-dev + +# Image processing libraries +libjpeg-dev +libpng-dev +libtiff-dev +libwebp-dev +libopenjp2-7-dev + +# Build tools (may be needed for some packages) +build-essential +cmake +pkg-config + +# Networking +curl +wget \ No newline at end of file