Compare commits
33 Commits
2534da67f0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 21722c5c85 | |||
| ad4d71e0b6 | |||
| d7f7df49e7 | |||
| 9c42b38c4b | |||
| 0aa1bb7069 | |||
| 35db99eb3d | |||
| b6e6190d6c | |||
| 26a9db889f | |||
| 26fc946a65 | |||
| a91b07ede4 | |||
| 185f3099ad | |||
| 5063b47a56 | |||
| e2eecb9cf9 | |||
| bd4f101fcc | |||
| cb861d0ffa | |||
| 02d13b2eaa | |||
| d2a996feb9 | |||
| 2a564f5e84 | |||
| 8d69a737f9 | |||
| 22901043b7 | |||
| f4c73b54f7 | |||
| f77f717af9 | |||
| 0c162bb0c8 | |||
| 1ea2ee584c | |||
| 027709618e | |||
| 8f64de240b | |||
| da91677f5b | |||
| 1579371395 | |||
| 99338b88c7 | |||
| 751a781634 | |||
| e2afbee438 | |||
| 1056fd2090 | |||
| 8fa2918905 |
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
venv/
|
||||
tkinter_app/
|
||||
59
ARCHITECTURE_INSTALL_GUIDE.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Multi-Architecture Offline Installation Guide
|
||||
|
||||
## Overview
|
||||
The enhanced `install_offline.sh` script automatically detects your system architecture and uses the appropriate libraries.
|
||||
|
||||
## Architecture Support
|
||||
|
||||
### 64-bit ARM (aarch64) - Raspberry Pi 4/5 with 64-bit OS
|
||||
- **Folder used**: `req_libraries/`
|
||||
- **Files**: 18 wheel files
|
||||
- **Key packages**:
|
||||
- bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl
|
||||
- pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.whl
|
||||
- psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.whl
|
||||
|
||||
### 32-bit ARM (armv7l) - Raspberry Pi with 32-bit OS
|
||||
- **Folder used**: `req_libraries_32bit/`
|
||||
- **Files**: 18 wheel files
|
||||
- **Key packages**:
|
||||
- bcrypt-4.3.0-cp311-cp311-linux_armv7l.whl
|
||||
- pillow-11.3.0-cp311-cp311-linux_armv7l.whl
|
||||
- psutil-6.0.0-cp311-abi3-linux_armv7l.whl
|
||||
|
||||
## Usage
|
||||
|
||||
### Simple Installation (Auto-Detect)
|
||||
```bash
|
||||
./install_offline.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Detect your architecture automatically
|
||||
2. Choose the correct library folder
|
||||
3. Install the appropriate packages
|
||||
4. Create a virtual environment
|
||||
5. Verify installation
|
||||
|
||||
### Manual Architecture Check
|
||||
```bash
|
||||
uname -m
|
||||
```
|
||||
- `aarch64` = 64-bit (uses req_libraries/)
|
||||
- `armv7l` = 32-bit (uses req_libraries_32bit/)
|
||||
|
||||
## Folder Structure
|
||||
```
|
||||
tkinter_player/
|
||||
├── req_libraries/ # 64-bit ARM wheels
|
||||
├── req_libraries_32bit/ # 32-bit ARM wheels
|
||||
├── install_offline.sh # Auto-detecting installer
|
||||
└── requirements.txt # Package list
|
||||
```
|
||||
|
||||
## Benefits
|
||||
- ✅ Single installer for both architectures
|
||||
- ✅ Automatic architecture detection
|
||||
- ✅ No manual intervention needed
|
||||
- ✅ Proper error messages for missing libraries
|
||||
- ✅ Complete offline installation support
|
||||
@@ -1,84 +0,0 @@
|
||||
|
||||
Here is the complete content for the `How to use.txt` file, starting from **point 3** and including all the missing information:
|
||||
|
||||
```plaintext
|
||||
### How to Install, Start, and Use the Signage Player Application
|
||||
|
||||
This guide provides step-by-step instructions for installing, starting, and using the signage player application.
|
||||
|
||||
---
|
||||
|
||||
## 3. Using the Application
|
||||
|
||||
### MediaPlayer Screen
|
||||
- **Play/Pause Button**:
|
||||
- Toggles between playing and pausing the media.
|
||||
- Automatically resumes playback after 30 seconds if not toggled again.
|
||||
- **Next/Previous Buttons**:
|
||||
- Navigate to the next or previous media in the playlist.
|
||||
- **Exit App Button**:
|
||||
- Opens a password-protected popup. Enter the `quickconnect_key` from the configuration file to exit the app.
|
||||
|
||||
### Settings Screen
|
||||
- **Configuration Fields**:
|
||||
- Update fields like `screen_orientation`, `screen_name`, `quickconnect_key`, `server_ip`, and `port`.
|
||||
- **Save Button**:
|
||||
- Saves the updated configuration and returns to the MediaPlayer screen.
|
||||
- **Exit App Button**:
|
||||
- Opens a password-protected popup. Enter the `quickconnect_key` to exit the app.
|
||||
|
||||
---
|
||||
|
||||
## 4. Configuration
|
||||
|
||||
### Configuration File
|
||||
The configuration file is located at:
|
||||
```bash
|
||||
app_config.txt
|
||||
```
|
||||
|
||||
### Example Configuration
|
||||
```json
|
||||
{
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "MyScreen",
|
||||
"quickconnect_key": "12345",
|
||||
"server_ip": "192.168.1.1",
|
||||
"port": "8080"
|
||||
}
|
||||
```
|
||||
|
||||
### Updating Configuration
|
||||
1. Navigate to the **Settings Screen** in the app.
|
||||
2. Update the fields and click **Save**.
|
||||
3. The configuration will be saved to `app_config.txt`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Troubleshooting
|
||||
|
||||
### Permission Denied for `run_app.sh`
|
||||
- Ensure the script is executable:
|
||||
```bash
|
||||
chmod +x /home/pi/Desktop/signage-player/run_app.sh
|
||||
```
|
||||
|
||||
### Application Does Not Start on Boot
|
||||
- Verify the `~/.bashrc` or `crontab` entry is correct.
|
||||
- Check the script's permissions and paths.
|
||||
|
||||
### Media Files Not Playing
|
||||
- Ensure media files are downloaded to the correct directory:
|
||||
```bash
|
||||
/home/pi/Desktop/signage-player/src/static/resurse
|
||||
```
|
||||
- Check the playlist configuration and server connection.
|
||||
|
||||
### Password for Exit App
|
||||
- The password is the `quickconnect_key` from the configuration file (`app_config.txt`).
|
||||
|
||||
---
|
||||
|
||||
Let me know if you need further clarification or additional details!
|
||||
```
|
||||
|
||||
44
OFFLINE_INSTALL_README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Offline Installation Guide
|
||||
|
||||
## Architecture Compatibility
|
||||
|
||||
### Current Libraries (req_libraries/)
|
||||
- Compatible with: 64-bit Raspberry Pi OS (aarch64)
|
||||
- NOT compatible with: 32-bit Raspberry Pi OS (armv7l)
|
||||
|
||||
### For 32-bit Raspberry Pi OS
|
||||
|
||||
The current wheels contain aarch64 packages that won't work on 32-bit systems.
|
||||
|
||||
## Installation Options
|
||||
|
||||
### 64-bit Raspberry Pi OS:
|
||||
```bash
|
||||
./install_offline.sh
|
||||
```
|
||||
|
||||
### 32-bit Raspberry Pi OS:
|
||||
1. Run on internet-connected system: `./download_32bit_libs.sh`
|
||||
2. Copy req_libraries_32bit/ to offline system
|
||||
3. Rename: `mv req_libraries_32bit req_libraries`
|
||||
4. Run: `./install_offline.sh`
|
||||
|
||||
## System Requirements
|
||||
|
||||
### 32-bit systems need build tools:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install build-essential python3-dev python3-pip
|
||||
```
|
||||
|
||||
### All systems need VLC:
|
||||
```bash
|
||||
sudo apt install vlc
|
||||
```
|
||||
|
||||
## Quick Check
|
||||
```bash
|
||||
uname -m
|
||||
```
|
||||
- aarch64 = 64-bit (current wheels work)
|
||||
- armv7l = 32-bit (need different wheels)
|
||||
150
README.md
@@ -1,150 +0,0 @@
|
||||
# 🎥 Kivy Media Player
|
||||
|
||||
A media player application built using **Kivy** that allows users to play video files, display images, and manage settings for quick connect codes and server configurations. The application checks for updates to the playlist every five minutes while running in full-screen mode.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Project Structure
|
||||
|
||||
```
|
||||
signage-player
|
||||
├── src
|
||||
│ ├── media_player.py # Handles media playback
|
||||
│ ├── python_functions.py # Utility functions for downloading files and updating playlists
|
||||
│ ├── kv
|
||||
│ │ └── media_player.kv # Layout for the media player screen
|
||||
│ ├── Resurse
|
||||
│ │ ├── app_config.txt # Configuration file
|
||||
│ │ ├── log.txt # Log file for media events
|
||||
│ │ ├── play.png # Play button icon
|
||||
│ │ ├── pause.png # Pause button icon
|
||||
│ │ └── other icons... # Additional icons for the UI
|
||||
│ └── static
|
||||
│ └── resurse # Directory for media files (images/videos)
|
||||
├── requirements.txt # Project dependencies
|
||||
├── run_app.sh # Script to run the application
|
||||
├── install.sh # Installation script
|
||||
├── How to use.txt # Detailed usage instructions
|
||||
└── README.md # Documentation for the project
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Setup Instructions
|
||||
|
||||
### Prerequisites
|
||||
Before installing the application, ensure the following are installed on your Raspberry Pi:
|
||||
- **Python 3.7 or higher**
|
||||
- **pip3** (Python package manager)
|
||||
- **ffmpeg** (for video conversion)
|
||||
- **Internet connection** for downloading dependencies
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. **Clone the Repository**
|
||||
```bash
|
||||
git clone https://gitea.moto-adv.com/ske087/signage-player.git
|
||||
cd signage-player
|
||||
```
|
||||
|
||||
2. **Run the Installation Script**
|
||||
```bash
|
||||
./install.sh
|
||||
```
|
||||
|
||||
The installation script will:
|
||||
- Update the system.
|
||||
- Install required Python and system dependencies.
|
||||
- Clone the repository to `/home/pi/Desktop/ds-player`.
|
||||
- Add the run_app.sh script to `~/.bashrc` for autostart.
|
||||
- Make the run_app.sh script executable.
|
||||
|
||||
3. **Reboot the Device**
|
||||
After installation, reboot the Raspberry Pi to start the application automatically:
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 How to Use
|
||||
|
||||
### MediaPlayer Screen
|
||||
- **▶️ Play/Pause Button**:
|
||||
- Toggles between playing and pausing the media.
|
||||
- Automatically resumes playback after 30 seconds if not toggled again.
|
||||
- **⏩ Next/⏪ Previous Buttons**:
|
||||
- Navigate to the next or previous media in the playlist.
|
||||
- **❌ Exit App Button**:
|
||||
- Opens a password-protected popup. Enter the `quickconnect_key` from the configuration file to exit the app.
|
||||
|
||||
### Settings Screen
|
||||
- **Configuration Fields**:
|
||||
- Update fields like `screen_orientation`, `screen_name`, `quickconnect_key`, `server_ip`, and `port`.
|
||||
- **💾 Save Button**:
|
||||
- Saves the updated configuration and returns to the MediaPlayer screen.
|
||||
- **❌ Exit App Button**:
|
||||
- Opens a password-protected popup. Enter the `quickconnect_key` to exit the app.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Configuration File
|
||||
The configuration file is located at:
|
||||
```bash
|
||||
/home/pi/Desktop/signage-player/src/Resurse/app_config.txt
|
||||
```
|
||||
|
||||
### Example Configuration
|
||||
```json
|
||||
{
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "tv-panou1",
|
||||
"quickconnect_key": "8887779",
|
||||
"server_ip": "172.20.10.9",
|
||||
"port": "80"
|
||||
}
|
||||
```
|
||||
|
||||
### Updating Configuration
|
||||
1. Navigate to the **Settings Screen** in the app.
|
||||
2. Update the fields and click **Save**.
|
||||
3. The configuration will be saved to app_config.txt.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Permission Denied for run_app.sh
|
||||
- Ensure the script is executable:
|
||||
```bash
|
||||
chmod +x /home/pi/Desktop/signage-player/run_app.sh
|
||||
```
|
||||
|
||||
### Application Does Not Start on Boot
|
||||
- Verify the `~/.bashrc` entry is correct.
|
||||
- Check the script's permissions and paths.
|
||||
|
||||
### Media Files Not Playing
|
||||
- Ensure media files are downloaded to the correct directory:
|
||||
```bash
|
||||
/home/pi/Desktop/signage-player/src/static/resurse
|
||||
```
|
||||
- Check the playlist configuration and server connection.
|
||||
|
||||
### Password for Exit App
|
||||
- The password is the `quickconnect_key` from the configuration file (`app_config.txt`).
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a pull request or open an issue for any suggestions or improvements.
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the **MIT License**. See the `LICENSE` file for more details.
|
||||
````
|
||||
29
build_pillow_local.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# Build Pillow from source and place wheel in req_libraries_32bit
|
||||
|
||||
set -e
|
||||
|
||||
echo "======================================="
|
||||
echo " BUILD PILLOW FROM SOURCE (32-bit) "
|
||||
echo "======================================="
|
||||
|
||||
# Ensure virtual environment is activated
|
||||
echo "Activating .venv..."
|
||||
source .venv/bin/activate
|
||||
|
||||
# Find Pillow source tar.gz
|
||||
PILLOW_SRC=$(ls req_libraries_32bit/Pillow-*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -z "$PILLOW_SRC" ]; then
|
||||
echo "❌ Pillow source distribution not found in req_libraries_32bit."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building Pillow from source: $PILLOW_SRC"
|
||||
pip wheel "$PILLOW_SRC" -w req_libraries_32bit
|
||||
|
||||
if ls req_libraries_32bit/pillow*.whl >/dev/null 2>&1; then
|
||||
echo "✅ Pillow wheel built successfully!"
|
||||
echo "You can now install it offline with ./install_32bit.sh."
|
||||
else
|
||||
echo "❌ Pillow wheel build failed. Check build dependencies."
|
||||
fi
|
||||
89
download_32bit_libs.sh
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
# Download 32-bit ARM compatible libraries with forced architecture
|
||||
|
||||
set -e
|
||||
|
||||
echo "======================================="
|
||||
echo " 32-BIT ARM LIBRARY DOWNLOADER"
|
||||
echo "======================================="
|
||||
echo ""
|
||||
|
||||
# Remove existing 32-bit folder to ensure clean download
|
||||
if [ -d "req_libraries_32bit" ]; then
|
||||
echo "🗑️ Removing existing req_libraries_32bit for clean download..."
|
||||
rm -rf req_libraries_32bit
|
||||
fi
|
||||
|
||||
# Create fresh directory
|
||||
mkdir -p req_libraries_32bit
|
||||
|
||||
echo "📦 Downloading 32-bit ARM compatible packages..."
|
||||
echo " Target architecture: linux_armv7l"
|
||||
echo ""
|
||||
|
||||
# Download with explicit 32-bit ARM platform targeting
|
||||
|
||||
echo "🔄 Downloading with forced 32-bit architecture (armv7l and armhf)..."
|
||||
|
||||
# Download for armv7l (wheels only)
|
||||
pip download -r requirements.txt -d req_libraries_32bit/ \
|
||||
--platform linux_armv7l \
|
||||
--only-binary=:all: \
|
||||
--python-version 311 \
|
||||
--abi cp311 || true
|
||||
# Download for armhf (wheels only)
|
||||
pip download -r requirements.txt -d req_libraries_32bit/ \
|
||||
--platform linux_armhf \
|
||||
--only-binary=:all: \
|
||||
--python-version 311 \
|
||||
--abi cp311 || true
|
||||
|
||||
# Download Pillow as source if wheel is not available
|
||||
echo "🔄 Downloading Pillow source distribution for local build..."
|
||||
pip download Pillow -d req_libraries_32bit/ --no-binary=:all: || true
|
||||
|
||||
echo ""
|
||||
echo "📊 Download results:"
|
||||
WHEEL_COUNT=$(ls req_libraries_32bit/*.whl 2>/dev/null | wc -l || echo "0")
|
||||
echo " Wheel files: $WHEEL_COUNT"
|
||||
|
||||
if [ "$WHEEL_COUNT" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "🔍 Key 32-bit packages:"
|
||||
if ls req_libraries_32bit/bcrypt*.whl >/dev/null 2>&1; then
|
||||
BCRYPT_FILE=$(basename $(ls req_libraries_32bit/bcrypt*.whl | head -1))
|
||||
echo " 🔐 BCRYPT: $BCRYPT_FILE"
|
||||
if [[ "$BCRYPT_FILE" == *"armv7l"* ]]; then
|
||||
echo " ✅ Correct 32-bit architecture"
|
||||
else
|
||||
echo " ⚠️ Architecture unclear"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ls req_libraries_32bit/pillow*.whl >/dev/null 2>&1; then
|
||||
PILLOW_FILE=$(basename $(ls req_libraries_32bit/pillow*.whl | head -1))
|
||||
echo " 🖼️ PILLOW: $PILLOW_FILE"
|
||||
if [[ "$PILLOW_FILE" == *"armv7l"* ]]; then
|
||||
echo " ✅ Correct 32-bit architecture"
|
||||
fi
|
||||
elif ls req_libraries_32bit/Pillow-*.tar.gz >/dev/null 2>&1; then
|
||||
PILLOW_SRC=$(basename $(ls req_libraries_32bit/Pillow-*.tar.gz | head -1))
|
||||
echo " 🖼️ PILLOW source: $PILLOW_SRC"
|
||||
echo " ⚠️ No wheel available for 32-bit ARM. Will need to build locally."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ 32-bit libraries download completed!"
|
||||
echo ""
|
||||
echo "📋 Next steps:"
|
||||
echo " 1. Copy this folder to your 32-bit Raspberry Pi"
|
||||
echo " 2. Run: ./install_32bit.sh"
|
||||
echo " 3. If Pillow wheel is missing, run: ./build_pillow_local.sh (see instructions below)"
|
||||
else
|
||||
echo "❌ No wheel files downloaded"
|
||||
echo ""
|
||||
echo "💡 Troubleshooting:"
|
||||
echo " • Check internet connection"
|
||||
echo " • Verify requirements.txt exists"
|
||||
echo " • Some packages may not have 32-bit wheels available"
|
||||
fi
|
||||
107
install_32bit.sh
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
# Dedicated Offline Installer for 32-bit Raspberry Pi OS (armv7l)
|
||||
# Ensures clean installation with proper architecture isolation
|
||||
|
||||
set -e
|
||||
|
||||
echo "================================="
|
||||
echo " TKINTER PLAYER 32-BIT INSTALL "
|
||||
echo "================================="
|
||||
|
||||
# 1. Check architecture
|
||||
ARCH=$(uname -m)
|
||||
echo "Architecture: $ARCH"
|
||||
if [ "$ARCH" != "armv7l" ]; then
|
||||
echo "ERROR: This script is for 32-bit Raspberry Pi OS (armv7l) only!"
|
||||
echo "Current architecture: $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Check library folder
|
||||
LIBS_FOLDER="req_libraries_32bit"
|
||||
if [ ! -d "$LIBS_FOLDER" ]; then
|
||||
echo "ERROR: $LIBS_FOLDER not found!"
|
||||
echo "Please download 32-bit libraries first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Library folder: $LIBS_FOLDER"
|
||||
WHEEL_COUNT=$(ls $LIBS_FOLDER/*.whl | wc -l)
|
||||
echo "Wheel files: $WHEEL_COUNT"
|
||||
|
||||
# 3. Show key 32-bit packages
|
||||
echo ""
|
||||
echo "32-bit packages to install:"
|
||||
if ls $LIBS_FOLDER/bcrypt*.whl >/dev/null 2>&1; then
|
||||
BCRYPT_FILE=$(basename $(ls $LIBS_FOLDER/bcrypt*.whl | head -1))
|
||||
echo " BCRYPT: $BCRYPT_FILE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 4. Clean existing virtual environment
|
||||
if [ -d ".venv" ]; then
|
||||
echo "Removing existing .venv to ensure clean installation..."
|
||||
rm -rf .venv
|
||||
fi
|
||||
|
||||
# 5. Create fresh virtual environment
|
||||
echo "Creating fresh virtual environment..."
|
||||
python3 -m venv .venv --clear
|
||||
|
||||
# 6. Activate environment
|
||||
echo "Activating virtual environment..."
|
||||
source .venv/bin/activate
|
||||
|
||||
# 7. Ensure we're using the virtual environment
|
||||
echo "Python location: $(which python)"
|
||||
echo "Pip location: $(which pip)"
|
||||
|
||||
# 8. Upgrade pip
|
||||
echo "Upgrading pip..."
|
||||
pip install --upgrade pip
|
||||
|
||||
# 9. Install packages with forced 32-bit isolation
|
||||
echo ""
|
||||
echo "Installing 32-bit packages (completely isolated)..."
|
||||
pip install --no-index --no-deps --force-reinstall --find-links $LIBS_FOLDER/ $LIBS_FOLDER/*.whl
|
||||
|
||||
# 10. Verify 32-bit installation
|
||||
echo ""
|
||||
echo "Verifying 32-bit installation..."
|
||||
python3 -c "
|
||||
import sys
|
||||
print(f'Python executable: {sys.executable}')
|
||||
print(f'Architecture: $(uname -m)')
|
||||
|
||||
try:
|
||||
import bcrypt
|
||||
print('✅ bcrypt imported successfully')
|
||||
# Test bcrypt functionality
|
||||
password = b'test'
|
||||
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
|
||||
if bcrypt.checkpw(password, hashed):
|
||||
print('✅ bcrypt functionality test passed')
|
||||
else:
|
||||
print('❌ bcrypt functionality test failed')
|
||||
except Exception as e:
|
||||
print(f'❌ bcrypt import/test failed: {e}')
|
||||
|
||||
try:
|
||||
import requests
|
||||
print('✅ requests imported successfully')
|
||||
except Exception as e:
|
||||
print(f'❌ requests failed: {e}')
|
||||
|
||||
try:
|
||||
import vlc
|
||||
print('✅ python-vlc imported successfully')
|
||||
except Exception as e:
|
||||
print(f'❌ python-vlc failed: {e}')
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "✅ 32-bit installation completed!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. source .venv/bin/activate"
|
||||
echo "2. ./run_app.sh"
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Minimal installer for Raspberry Pi OS (no desktop environment)
|
||||
# Installs Xorg, Openbox, disables power saving, and configures auto-launch of the signage player
|
||||
|
||||
set -e
|
||||
|
||||
USER_HOME="/home/pi"
|
||||
PROJECT_DIR="$USER_HOME/Desktop/signage-player"
|
||||
APP_LAUNCH_SCRIPT="$PROJECT_DIR/run_tkinter_app.sh"
|
||||
|
||||
# Update system
|
||||
sudo apt update
|
||||
sudo apt upgrade -y
|
||||
|
||||
# Install minimal X server and Openbox
|
||||
sudo apt install -y xorg openbox
|
||||
|
||||
# Install VLC for video playback
|
||||
sudo apt install -y vlc
|
||||
|
||||
# Install Python and dependencies
|
||||
sudo apt install -y python3 python3-pip python3-venv python3-tk ffmpeg libopencv-dev python3-opencv \
|
||||
libsdl2-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-ttf-dev \
|
||||
libjpeg-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev
|
||||
|
||||
# Create virtual environment and install Python requirements
|
||||
cd "$PROJECT_DIR"
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r tkinter_requirements.txt
|
||||
pip install python-vlc
|
||||
chmod +x "$APP_LAUNCH_SCRIPT"
|
||||
deactivate
|
||||
68
install_offline.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# Simple Offline Installation Script for Tkinter Player
|
||||
|
||||
set -e
|
||||
|
||||
echo "================================="
|
||||
echo " TKINTER PLAYER OFFLINE INSTALL"
|
||||
echo "================================="
|
||||
|
||||
# 1. Check architecture
|
||||
|
||||
# Detect architecture, treating armhf and armv7l as 32-bit
|
||||
ARCH=$(uname -m)
|
||||
if lscpu | grep -qi 'armhf'; then
|
||||
ARCH_TYPE="armhf"
|
||||
else
|
||||
ARCH_TYPE="$ARCH"
|
||||
fi
|
||||
echo "Architecture: $ARCH_TYPE"
|
||||
|
||||
# 2. Select library folder
|
||||
|
||||
# Use 32-bit libraries for armv7l or armhf
|
||||
if [ "$ARCH_TYPE" = "armv7l" ] || [ "$ARCH_TYPE" = "armhf" ]; then
|
||||
LIBS_FOLDER="req_libraries_32bit"
|
||||
echo "Using: 32-bit libraries (armv7l/armhf)"
|
||||
elif [ "$ARCH_TYPE" = "aarch64" ]; then
|
||||
LIBS_FOLDER="req_libraries"
|
||||
echo "Using: 64-bit libraries (aarch64)"
|
||||
else
|
||||
LIBS_FOLDER="req_libraries"
|
||||
echo "Using: default libraries"
|
||||
fi
|
||||
|
||||
# 3. Check folder exists
|
||||
if [ ! -d "$LIBS_FOLDER" ]; then
|
||||
echo "ERROR: $LIBS_FOLDER not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Library folder: $LIBS_FOLDER"
|
||||
WHEEL_COUNT=$(ls $LIBS_FOLDER/*.whl | wc -l)
|
||||
echo "Wheel files: $WHEEL_COUNT"
|
||||
|
||||
# 4. Create .venv
|
||||
echo ""
|
||||
echo "Creating .venv..."
|
||||
python3 -m venv .venv
|
||||
|
||||
# 5. Activate environment
|
||||
echo "Activating environment..."
|
||||
source .venv/bin/activate
|
||||
|
||||
# 6. Upgrade pip
|
||||
#echo "Upgrading pip..."
|
||||
#pip install --upgrade pip
|
||||
|
||||
# 7. Install packages offline
|
||||
echo ""
|
||||
echo "Installing from $LIBS_FOLDER..."
|
||||
pip install --no-index --no-deps --find-links $LIBS_FOLDER/ $LIBS_FOLDER/*.whl
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation completed!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. source .venv/bin/activate"
|
||||
echo "2. ./run_app.sh"
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Tkinter Media Player Installation Script
|
||||
|
||||
echo "Installing Tkinter Media Player..."
|
||||
|
||||
# Update system packages
|
||||
echo "Updating system packages..."
|
||||
sudo apt update
|
||||
sudo apt upgrade -y
|
||||
|
||||
# Install system dependencies
|
||||
echo "Installing system dependencies..."
|
||||
sudo apt install -y python3 python3-pip python3-venv python3-tk
|
||||
sudo apt install -y ffmpeg libopencv-dev python3-opencv
|
||||
sudo apt install -y libsdl2-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-ttf-dev
|
||||
sudo apt install -y libjpeg-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev
|
||||
|
||||
# Create project directory if it doesn't exist
|
||||
PROJECT_DIR="/home/pi/Desktop/tkinter_player"
|
||||
if [ ! -d "$PROJECT_DIR" ]; then
|
||||
echo "Project directory not found. Please ensure the tkinter_player directory exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Create virtual environment
|
||||
echo "Creating Python virtual environment..."
|
||||
python3 -m venv venv
|
||||
|
||||
# Activate virtual environment and install requirements
|
||||
echo "Installing Python dependencies..."
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r tkinter_requirements.txt
|
||||
deactivate
|
||||
|
||||
# Make launcher script executable
|
||||
chmod +x run_tkinter_app.sh
|
||||
|
||||
# Create systemd service for auto-start
|
||||
echo "Creating systemd service..."
|
||||
sudo tee /etc/systemd/system/tkinter-signage-player.service > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=Tkinter Signage Player
|
||||
After=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Environment=DISPLAY=:0
|
||||
ExecStart=/home/pi/Desktop/signage-player/run_tkinter_app.sh
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
EOF
|
||||
|
||||
# Enable the service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable tkinter-signage-player.service
|
||||
|
||||
echo "Installation completed!"
|
||||
echo "The tkinter media player will start automatically on boot."
|
||||
echo "To start manually, run: ./run_tkinter_app.sh"
|
||||
echo "To stop the service: sudo systemctl stop tkinter-signage-player.service"
|
||||
echo "To view logs: sudo journalctl -u tkinter-signage-player.service -f"
|
||||
BIN
req_libraries/MouseInfo-0.1.3-py3-none-any.whl
Normal file
BIN
req_libraries/PyAutoGUI-0.9.54-py3-none-any.whl
Normal file
BIN
req_libraries/PyGetWindow-0.0.9-py3-none-any.whl
Normal file
BIN
req_libraries/PyRect-0.2.0-py2.py3-none-any.whl
Normal file
BIN
req_libraries/PyScreeze-1.0.1-py3-none-any.whl
Normal file
BIN
req_libraries/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl
Normal file
BIN
req_libraries/pymsgbox-2.0.1-py3-none-any.whl
Normal file
BIN
req_libraries/pyperclip-1.9.0-py3-none-any.whl
Normal file
BIN
req_libraries/python3_xlib-0.15-py3-none-any.whl
Normal file
BIN
req_libraries/python_vlc-3.0.21203-py3-none-any.whl
Normal file
BIN
req_libraries/pytweening-1.2.0-py3-none-any.whl
Normal file
BIN
req_libraries/requests-2.32.5-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/MouseInfo-0.1.3-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/PyAutoGUI-0.9.54-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/PyGetWindow-0.0.9-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/PyRect-0.2.0-py2.py3-none-any.whl
Normal file
BIN
req_libraries_32bit/PyScreeze-1.0.1-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/bcrypt-4.3.0-cp311-cp311-linux_armv7l.whl
Normal file
BIN
req_libraries_32bit/certifi-2025.8.3-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/idna-3.10-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/pillow-11.3.0-cp311-cp311-linux_armv7l.whl
Normal file
BIN
req_libraries_32bit/pillow-11.3.0.tar.gz
Normal file
BIN
req_libraries_32bit/pymsgbox-2.0.1-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/pyperclip-1.9.0-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/python3_xlib-0.15-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/python_vlc-3.0.21203-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/pytweening-1.2.0-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/requests-2.32.5-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/setuptools-80.9.0-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/urllib3-2.5.0-py3-none-any.whl
Normal file
BIN
req_libraries_32bit/wheel-0.45.1-py3-none-any.whl
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
python-vlc
|
||||
Pillow
|
||||
pyautogui
|
||||
requests
|
||||
bcrypt
|
||||
setuptools
|
||||
wheel
|
||||
35
run_app.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Launch script for the tkinter signage player
|
||||
|
||||
# Get script directory and navigate there
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=== Tkinter Signage Player Launcher ==="
|
||||
echo "Working directory: $SCRIPT_DIR"
|
||||
|
||||
# Activate virtual environment
|
||||
if [ -d ".venv" ]; then
|
||||
echo "Activating virtual environment..."
|
||||
source .venv/bin/activate
|
||||
echo "Virtual environment activated"
|
||||
else
|
||||
echo "Warning: No virtual environment found"
|
||||
fi
|
||||
|
||||
# Set display environment
|
||||
export DISPLAY=${DISPLAY:-:0.0}
|
||||
|
||||
# Create directories if needed
|
||||
mkdir -p signage_player/main_data
|
||||
|
||||
# Change to signage_player directory
|
||||
cd signage_player
|
||||
|
||||
echo "Starting application..."
|
||||
python main.py
|
||||
|
||||
# Deactivate venv when done
|
||||
if [ -d "../.venv" ]; then
|
||||
deactivate
|
||||
fi
|
||||
@@ -1,14 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Debugging launch script for the tkinter player application
|
||||
|
||||
# Activate the virtual environment
|
||||
source venv/bin/activate
|
||||
# Set working directory to script location
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Change to the tkinter app src directory
|
||||
cd tkinter_app/src
|
||||
# Activate the virtual environment if it exists
|
||||
if [ -d "venv" ]; then
|
||||
source venv/bin/activate
|
||||
echo "Virtual environment activated"
|
||||
else
|
||||
echo "Warning: No virtual environment found, using system Python"
|
||||
fi
|
||||
|
||||
# Set environment variables for better display on Raspberry Pi
|
||||
export DISPLAY=${DISPLAY:-:0.0}
|
||||
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/tmp/runtime-pi}
|
||||
|
||||
# Change to the signage_player directory
|
||||
cd signage_player
|
||||
|
||||
# Run the main application with full error output
|
||||
python main.py
|
||||
echo "Starting tkinter player..."
|
||||
echo "Working directory: $(pwd)"
|
||||
echo "Python path: $(which python3)"
|
||||
echo "Display: $DISPLAY"
|
||||
|
||||
# Run with error logging
|
||||
python3 main.py 2>&1 | tee ../main_data/app_debug.log
|
||||
|
||||
# Capture exit code
|
||||
EXIT_CODE=$?
|
||||
|
||||
# Deactivate virtual environment when done
|
||||
deactivate
|
||||
if [ -d "../venv" ]; then
|
||||
deactivate
|
||||
fi
|
||||
|
||||
echo "Application exited with code: $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
BIN
signage_player/__pycache__/get_playlists.cpython-311.pyc
Normal file
BIN
signage_player/__pycache__/logging_config.cpython-311.pyc
Normal file
BIN
signage_player/__pycache__/player.cpython-311.pyc
Normal file
112
signage_player/appsettings.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import json
|
||||
import os
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
||||
|
||||
class AppSettingsWindow(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title('App Settings')
|
||||
self.geometry('440x600') # Increased height for better button visibility
|
||||
self.resizable(False, False)
|
||||
self.config(bg='#23272e')
|
||||
|
||||
# Ensure window appears on top and gets focus
|
||||
self.attributes('-topmost', True)
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
self.grab_set() # Make window modal
|
||||
|
||||
# Center the window on screen
|
||||
self.center_window()
|
||||
|
||||
self.fields = {}
|
||||
self.load_config()
|
||||
self.style = ttk.Style(self)
|
||||
self.set_styles()
|
||||
self.create_widgets()
|
||||
|
||||
# Ensure focus after widgets are created
|
||||
self.after(100, self.focus_force)
|
||||
|
||||
def center_window(self):
|
||||
"""Center the settings window on the screen"""
|
||||
self.update_idletasks()
|
||||
width = self.winfo_width()
|
||||
height = self.winfo_height()
|
||||
x = (self.winfo_screenwidth() // 2) - (width // 2)
|
||||
y = (self.winfo_screenheight() // 2) - (height // 2)
|
||||
self.geometry(f'{width}x{height}+{x}+{y}')
|
||||
|
||||
def set_styles(self):
|
||||
self.style.theme_use('clam')
|
||||
self.style.configure('TLabel', background='#23272e', foreground='#e0e0e0', font=('Segoe UI', 13, 'bold'))
|
||||
self.style.configure('TEntry', fieldbackground='#2c313c', foreground='#e0e0e0', borderwidth=1, relief='flat', font=('Segoe UI', 12))
|
||||
self.style.map('TEntry', fieldbackground=[('active', '#23272e')])
|
||||
self.style.configure('TButton', background='#3b82f6', foreground='white', font=('Segoe UI', 13, 'bold'), borderwidth=0, focusthickness=3, focuscolor='#60a5fa', padding=8)
|
||||
self.style.map('TButton', background=[('active', '#2563eb')])
|
||||
|
||||
def load_config(self):
|
||||
try:
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
self.config_data = json.load(f)
|
||||
except Exception as e:
|
||||
self.config_data = {}
|
||||
messagebox.showerror('Error', f'Failed to load config: {e}')
|
||||
|
||||
def save_config(self):
|
||||
try:
|
||||
for key, entry in self.fields.items():
|
||||
if hasattr(entry, 'get'):
|
||||
self.config_data[key] = entry.get()
|
||||
with open(CONFIG_PATH, 'w') as f:
|
||||
json.dump(self.config_data, f, indent=4)
|
||||
self.show_custom_popup('Success', 'Settings saved!')
|
||||
except Exception as e:
|
||||
messagebox.showerror('Error', f'Failed to save config: {e}')
|
||||
|
||||
def show_custom_popup(self, title, message):
|
||||
popup = tk.Toplevel(self)
|
||||
popup.title(title)
|
||||
popup.geometry('320x120')
|
||||
popup.configure(bg='#23272e')
|
||||
popup.resizable(False, False)
|
||||
popup.attributes('-topmost', True)
|
||||
ttk.Label(popup, text=title, style='TLabel').pack(pady=(18, 0))
|
||||
ttk.Label(popup, text=message, style='TLabel').pack(pady=(8, 0))
|
||||
close_btn = ttk.Button(popup, text='OK', style='TButton', command=popup.destroy)
|
||||
close_btn.pack(pady=18)
|
||||
popup.grab_set()
|
||||
popup.after(5000, popup.destroy)
|
||||
|
||||
def create_widgets(self):
|
||||
title = ttk.Label(self, text='Application Settings', style='TLabel')
|
||||
title.grid(row=0, column=0, columnspan=2, pady=(18, 6))
|
||||
warning = tk.Label(self, text='⚠️ Modify values only if necessary or instructed!', fg='#ffb300', bg='#23272e', font=('Segoe UI', 11, 'bold'))
|
||||
warning.grid(row=1, column=0, columnspan=2, pady=(0, 18))
|
||||
row = 2
|
||||
for key, value in self.config_data.items():
|
||||
label = ttk.Label(self, text=key+':', style='TLabel')
|
||||
label.grid(row=row, column=0, sticky='e', padx=18, pady=10)
|
||||
if key == 'touch_screen':
|
||||
var = tk.StringVar()
|
||||
combo = ttk.Combobox(self, textvariable=var, values=['On', 'Off'], state='readonly', width=26)
|
||||
combo.set(str(value))
|
||||
combo.grid(row=row, column=1, padx=10, pady=10)
|
||||
self.fields[key] = combo
|
||||
else:
|
||||
entry = ttk.Entry(self, style='TEntry', width=28)
|
||||
entry.insert(0, str(value))
|
||||
entry.grid(row=row, column=1, padx=10, pady=10)
|
||||
self.fields[key] = entry
|
||||
row += 1
|
||||
save_btn = ttk.Button(self, text='Save Settings', style='TButton', command=self.save_config)
|
||||
save_btn.grid(row=row, column=0, pady=30, sticky='e', padx=(0,10))
|
||||
close_btn = ttk.Button(self, text='Close', style='TButton', command=self.destroy)
|
||||
close_btn.grid(row=row, column=1, pady=30, sticky='w', padx=(10,0))
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = AppSettingsWindow()
|
||||
app.mainloop()
|
||||
399
signage_player/get_playlists.py
Normal file
@@ -0,0 +1,399 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import bcrypt
|
||||
import re
|
||||
import datetime
|
||||
from logging_config import Logger
|
||||
|
||||
# Global variable to track server connectivity status
|
||||
SERVER_CONNECTION_STATUS = {
|
||||
'is_online': True,
|
||||
'last_successful_connection': None,
|
||||
'last_playlist_update': None,
|
||||
'error_message': None
|
||||
}
|
||||
|
||||
def get_server_status():
|
||||
"""Get current server connection status"""
|
||||
return SERVER_CONNECTION_STATUS.copy()
|
||||
|
||||
def get_last_playlist_update_time():
|
||||
"""Get the timestamp of the last playlist update from filesystem"""
|
||||
try:
|
||||
playlist_dir = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||
if os.path.exists(playlist_dir):
|
||||
playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
if playlist_files:
|
||||
# Get the most recent playlist file
|
||||
latest_file = max([os.path.join(playlist_dir, f) for f in playlist_files], key=os.path.getmtime)
|
||||
mod_time = os.path.getmtime(latest_file)
|
||||
return datetime.datetime.fromtimestamp(mod_time)
|
||||
return None
|
||||
except Exception as e:
|
||||
Logger.error(f"Error getting last playlist update time: {e}")
|
||||
return None
|
||||
|
||||
def set_server_offline(error_message=None):
|
||||
"""Mark server as offline with optional error message"""
|
||||
global SERVER_CONNECTION_STATUS
|
||||
SERVER_CONNECTION_STATUS['is_online'] = False
|
||||
SERVER_CONNECTION_STATUS['error_message'] = error_message
|
||||
Logger.warning(f"Server marked as offline: {error_message}")
|
||||
|
||||
def set_server_online():
|
||||
"""Mark server as online and update connection time"""
|
||||
global SERVER_CONNECTION_STATUS
|
||||
SERVER_CONNECTION_STATUS['is_online'] = True
|
||||
SERVER_CONNECTION_STATUS['last_successful_connection'] = datetime.datetime.now()
|
||||
SERVER_CONNECTION_STATUS['error_message'] = None
|
||||
Logger.info("Server connection restored")
|
||||
|
||||
def send_player_feedback(config, message, status="active", playlist_version=None, error_details=None):
|
||||
"""
|
||||
Send feedback to the server about player status.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
message (str): Main feedback message
|
||||
status (str): Player status - "active", "playing", "error", "restarting"
|
||||
playlist_version (int, optional): Current playlist version being played
|
||||
error_details (str, optional): Error details if status is "error"
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
server = config.get("server_ip", "")
|
||||
host = config.get("screen_name", "")
|
||||
quick = config.get("quickconnect_key", "")
|
||||
port = config.get("port", "")
|
||||
|
||||
# Construct server URL
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server):
|
||||
feedback_url = f'http://{server}:{port}/api/player-feedback'
|
||||
else:
|
||||
feedback_url = f'http://{server}/api/player-feedback'
|
||||
|
||||
# Prepare feedback data
|
||||
feedback_data = {
|
||||
'player_name': host,
|
||||
'quickconnect_code': quick,
|
||||
'message': message,
|
||||
'status': status,
|
||||
'timestamp': datetime.datetime.now().isoformat(),
|
||||
'playlist_version': playlist_version,
|
||||
'error_details': error_details
|
||||
}
|
||||
|
||||
Logger.info(f"Sending feedback to {feedback_url}: {feedback_data}")
|
||||
|
||||
# Send POST request
|
||||
response = requests.post(feedback_url, json=feedback_data, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
Logger.info(f"Feedback sent successfully: {message}")
|
||||
# Mark server as online on successful feedback
|
||||
set_server_online()
|
||||
return True
|
||||
else:
|
||||
Logger.warning(f"Feedback failed with status {response.status_code}: {response.text}")
|
||||
set_server_offline(f"Feedback failed with status {response.status_code}")
|
||||
return False
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"Failed to send feedback: {e}")
|
||||
set_server_offline(f"Network error during feedback: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
Logger.error(f"Unexpected error sending feedback: {e}")
|
||||
set_server_offline(f"Unexpected error during feedback: {e}")
|
||||
return False
|
||||
|
||||
def send_playlist_check_feedback(config, playlist_version=None):
|
||||
"""
|
||||
Send feedback when server is interrogated for playlist updates.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, server interrogation, checking for updates : {version_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="active",
|
||||
playlist_version=playlist_version
|
||||
)
|
||||
|
||||
def send_playlist_restart_feedback(config, playlist_version=None):
|
||||
"""
|
||||
Send feedback when playlist loop ends and restarts.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, playlist working in loop, cycle completed : {version_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="restarting",
|
||||
playlist_version=playlist_version
|
||||
)
|
||||
|
||||
def send_player_error_feedback(config, error_message, playlist_version=None):
|
||||
"""
|
||||
Send feedback when an error occurs in the player.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
error_message (str): Description of the error
|
||||
playlist_version (int, optional): Current playlist version
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
message = f"player {player_name}, error occurred"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="error",
|
||||
playlist_version=playlist_version,
|
||||
error_details=error_message
|
||||
)
|
||||
|
||||
def send_playing_status_feedback(config, playlist_version=None, current_media=None):
|
||||
"""
|
||||
Send feedback about playlist starting (first media).
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
current_media (str, optional): First media file in playlist
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, playlist started : {version_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="playing",
|
||||
playlist_version=playlist_version
|
||||
)
|
||||
|
||||
def is_playlist_up_to_date(local_playlist_path, config):
|
||||
"""
|
||||
Compare the version of the local playlist with the server playlist.
|
||||
Returns True if up-to-date, False otherwise.
|
||||
"""
|
||||
import json
|
||||
if not os.path.exists(local_playlist_path):
|
||||
Logger.info(f"Local playlist file not found: {local_playlist_path}")
|
||||
return False
|
||||
with open(local_playlist_path, 'r') as f:
|
||||
local_data = json.load(f)
|
||||
local_version = local_data.get('version', 0)
|
||||
server_data = fetch_server_playlist(config)
|
||||
server_version = server_data.get('version', 0)
|
||||
Logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}")
|
||||
return local_version == server_version
|
||||
|
||||
def fetch_server_playlist(config):
|
||||
"""Fetch the updated playlist from the server using a config dict."""
|
||||
server = config.get("server_ip", "")
|
||||
host = config.get("screen_name", "")
|
||||
quick = config.get("quickconnect_key", "")
|
||||
port = config.get("port", "")
|
||||
try:
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server):
|
||||
server_url = f'http://{server}:{port}/api/playlists'
|
||||
else:
|
||||
server_url = f'http://{server}/api/playlists'
|
||||
params = {
|
||||
'hostname': host,
|
||||
'quickconnect_code': quick
|
||||
}
|
||||
Logger.info(f"Fetching playlist from URL: {server_url} with params: {params}")
|
||||
response = requests.get(server_url, params=params, timeout=15)
|
||||
|
||||
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.")
|
||||
# Mark server as online on successful connection
|
||||
set_server_online()
|
||||
return {'playlist': playlist, 'version': version}
|
||||
else:
|
||||
Logger.error("Quickconnect code validation failed.")
|
||||
set_server_offline("Authentication failed - invalid quickconnect code")
|
||||
else:
|
||||
Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
|
||||
set_server_offline("Invalid server response - missing playlist data")
|
||||
else:
|
||||
Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
|
||||
set_server_offline(f"Server returned error code: {response.status_code}")
|
||||
|
||||
except requests.exceptions.ConnectTimeout as e:
|
||||
Logger.error(f"Connection timeout while fetching playlist: {e}")
|
||||
set_server_offline("Connection timeout - server unreachable")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
Logger.error(f"Connection error while fetching playlist: {e}")
|
||||
set_server_offline("Connection failed - server unreachable")
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"Request error while fetching playlist: {e}")
|
||||
set_server_offline(f"Network error: {str(e)}")
|
||||
except Exception as e:
|
||||
Logger.error(f"Unexpected error while fetching playlist: {e}")
|
||||
set_server_offline(f"Unexpected error: {str(e)}")
|
||||
|
||||
return {'playlist': [], 'version': 0}
|
||||
|
||||
def save_playlist_with_version(playlist_data, playlist_dir):
|
||||
version = playlist_data.get('version', 0)
|
||||
playlist_file = os.path.join(playlist_dir, f'server_playlist_v{version}.json')
|
||||
with open(playlist_file, 'w') as f:
|
||||
json.dump(playlist_data, f, indent=2)
|
||||
print(f"Playlist saved to {playlist_file}")
|
||||
return playlist_file
|
||||
|
||||
|
||||
def download_media_files(playlist, media_dir):
|
||||
"""Download media files from the server and save them to media_dir."""
|
||||
if not os.path.exists(media_dir):
|
||||
os.makedirs(media_dir)
|
||||
Logger.info(f"Created directory {media_dir} for media files.")
|
||||
|
||||
updated_playlist = []
|
||||
for media in playlist:
|
||||
file_name = media.get('file_name', '')
|
||||
file_url = media.get('url', '')
|
||||
duration = media.get('duration', 10)
|
||||
local_path = os.path.join(media_dir, file_name)
|
||||
Logger.info(f"Preparing to download {file_name} from {file_url}...")
|
||||
if os.path.exists(local_path):
|
||||
Logger.info(f"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"Successfully downloaded {file_name} to {local_path}")
|
||||
else:
|
||||
Logger.error(f"Failed to download {file_name}. Status Code: {response.status_code}")
|
||||
continue
|
||||
except requests.exceptions.RequestException as e:
|
||||
Logger.error(f"Error downloading {file_name}: {e}")
|
||||
continue
|
||||
updated_media = {
|
||||
'file_name': file_name,
|
||||
'url': os.path.relpath(local_path, os.path.dirname(media_dir)),
|
||||
'duration': duration
|
||||
}
|
||||
updated_playlist.append(updated_media)
|
||||
return updated_playlist
|
||||
|
||||
def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, keep_versions=1):
|
||||
"""
|
||||
Delete old playlist files and media files not referenced by the latest playlist version.
|
||||
keep_versions: number of latest versions to keep (default 1)
|
||||
"""
|
||||
# Find all playlist files
|
||||
playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
# Keep only the latest N versions
|
||||
versions = sorted([int(f.split('_v')[-1].split('.json')[0]) for f in playlist_files], reverse=True)
|
||||
keep = set(versions[:keep_versions])
|
||||
# Delete old playlist files
|
||||
for f in playlist_files:
|
||||
v = int(f.split('_v')[-1].split('.json')[0])
|
||||
if v not in keep:
|
||||
os.remove(os.path.join(playlist_dir, f))
|
||||
# Collect all media files referenced by the kept playlists
|
||||
referenced = set()
|
||||
for v in keep:
|
||||
path = os.path.join(playlist_dir, f'server_playlist_v{v}.json')
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
data = json.load(f)
|
||||
for item in data.get('playlist', []):
|
||||
referenced.add(item.get('file_name'))
|
||||
# Delete media files not referenced
|
||||
for f in os.listdir(media_dir):
|
||||
if f not in referenced:
|
||||
try:
|
||||
os.remove(os.path.join(media_dir, f))
|
||||
except Exception as e:
|
||||
Logger.warning(f"Failed to delete media file {f}: {e}")
|
||||
|
||||
def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir):
|
||||
"""
|
||||
Fetch the server playlist once, compare versions, and update if needed.
|
||||
Returns True if updated, False if already up to date.
|
||||
Also sends feedback to server about playlist check.
|
||||
"""
|
||||
import json
|
||||
global SERVER_CONNECTION_STATUS
|
||||
|
||||
server_data = fetch_server_playlist(config)
|
||||
server_version = server_data.get('version', 0)
|
||||
if not os.path.exists(local_playlist_path):
|
||||
local_version = 0
|
||||
else:
|
||||
with open(local_playlist_path, 'r') as f:
|
||||
local_data = json.load(f)
|
||||
local_version = local_data.get('version', 0)
|
||||
|
||||
Logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}")
|
||||
|
||||
# Only send feedback if server is online
|
||||
if SERVER_CONNECTION_STATUS['is_online']:
|
||||
send_playlist_check_feedback(config, server_version if server_version > 0 else local_version)
|
||||
|
||||
if local_version != server_version and server_version > 0:
|
||||
if server_data and server_data.get('playlist'):
|
||||
updated_playlist = download_media_files(server_data['playlist'], media_dir)
|
||||
server_data['playlist'] = updated_playlist
|
||||
save_playlist_with_version(server_data, playlist_dir)
|
||||
# Delete old playlists and unreferenced media
|
||||
delete_old_playlists_and_media(server_version, playlist_dir, media_dir)
|
||||
|
||||
# Update last playlist update time
|
||||
SERVER_CONNECTION_STATUS['last_playlist_update'] = datetime.datetime.now()
|
||||
|
||||
return True
|
||||
else:
|
||||
Logger.warning("No playlist data fetched from server or playlist is empty.")
|
||||
return False
|
||||
else:
|
||||
Logger.info("Local playlist is already up to date or server is offline.")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
19
signage_player/logging_config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Path to the log file (in main_data for new app)
|
||||
LOG_FILE_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'log.txt')
|
||||
|
||||
Logger = logging.getLogger('SignageApp')
|
||||
Logger.setLevel(logging.INFO)
|
||||
|
||||
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a')
|
||||
file_handler.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('[%(levelname)s] [%(name)s] %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
Logger.addHandler(file_handler)
|
||||
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.INFO)
|
||||
stream_handler.setFormatter(formatter)
|
||||
Logger.addHandler(stream_handler)
|
||||
75
signage_player/main.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
from get_playlists import update_playlist_if_needed
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
||||
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
|
||||
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||
LOCAL_PLAYLIST_PATH = os.path.join(PLAYLIST_DIR, 'server_playlist.json')
|
||||
|
||||
def playlist_update_loop(refresh_time, app_running, config, local_playlist_path, media_dir, playlist_dir):
|
||||
while app_running[0]:
|
||||
updated = update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir)
|
||||
if updated:
|
||||
print(f"[REFRESH] Playlist updated from server at {time.strftime('%X')}")
|
||||
else:
|
||||
print(f"[REFRESH] Playlist already up to date at {time.strftime('%X')}")
|
||||
time.sleep(refresh_time)
|
||||
|
||||
|
||||
import tkinter as tk
|
||||
from player import SimpleTkPlayer, load_latest_playlist
|
||||
|
||||
def main():
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
config = json.load(f)
|
||||
refresh_time = int(config.get('refresh_time', 5)) * 60 # minutes
|
||||
app_running = [True]
|
||||
update_thread = threading.Thread(
|
||||
target=playlist_update_loop,
|
||||
args=(refresh_time, app_running, config, LOCAL_PLAYLIST_PATH, MEDIA_DATA_PATH, PLAYLIST_DIR),
|
||||
daemon=True
|
||||
)
|
||||
update_thread.start()
|
||||
|
||||
root = tk.Tk()
|
||||
root.title("Simple Tkinter Player")
|
||||
root.configure(bg='black')
|
||||
root.attributes('-fullscreen', True)
|
||||
|
||||
# Load playlist and create player
|
||||
playlist = load_latest_playlist()
|
||||
player = SimpleTkPlayer(root, playlist)
|
||||
player.app_running = app_running
|
||||
orig_exit_app = player.exit_app
|
||||
def exit_and_stop():
|
||||
app_running[0] = False
|
||||
orig_exit_app()
|
||||
player.exit_app = exit_and_stop
|
||||
player.exit_btn.config(command=player.exit_app)
|
||||
player.main_start()
|
||||
|
||||
def reload_playlist_if_updated():
|
||||
new_playlist = load_latest_playlist()
|
||||
if new_playlist != player.playlist:
|
||||
print("[MAIN] Playlist updated, reloading...")
|
||||
player.playlist = new_playlist
|
||||
player.current_index = 0
|
||||
# Only restart if we're not already in a transition
|
||||
if not hasattr(player, 'is_transitioning') or not player.is_transitioning:
|
||||
player.show_current_media()
|
||||
root.after(10000, reload_playlist_if_updated)
|
||||
reload_playlist_if_updated()
|
||||
|
||||
try:
|
||||
root.mainloop()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
app_running[0] = False
|
||||
# Do not join the update_thread; let it exit as a daemon
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -2,9 +2,10 @@
|
||||
"screen_orientation": "Landscape",
|
||||
"screen_name": "tv-terasa",
|
||||
"quickconnect_key": "8887779",
|
||||
"server_ip": "digi-signage.moto-adv.com",
|
||||
"port": "8880",
|
||||
"server_ip": "192.168.1.22",
|
||||
"port": "80",
|
||||
"screen_w": "1920",
|
||||
"screen_h": "1080",
|
||||
"playlist_version": 0
|
||||
"refresh_time": "5",
|
||||
"touch_screen": "On"
|
||||
}
|
||||
21260
signage_player/main_data/log.txt
Normal file
353
signage_player/old_code/player copy vineri.py
Normal file
@@ -0,0 +1,353 @@
|
||||
import os
|
||||
import json
|
||||
import tkinter as tk
|
||||
import vlc
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
||||
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
|
||||
|
||||
class SimpleTkPlayer:
|
||||
def __init__(self, root, playlist):
|
||||
self.root = root
|
||||
self.playlist = playlist
|
||||
self.current_index = 0
|
||||
self.paused = False
|
||||
self.pause_timer = None
|
||||
self.label = tk.Label(root, bg='black')
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
self.create_controls()
|
||||
self.hide_controls()
|
||||
self.root.bind('<Motion>', self.on_activity)
|
||||
self.root.bind('<Button-1>', self.on_activity)
|
||||
self.root.after(100, self.ensure_fullscreen)
|
||||
self.root.after(200, self.hide_mouse)
|
||||
self.root.after(300, self.move_mouse_to_corner)
|
||||
self.root.protocol('WM_DELETE_WINDOW', self.exit_app)
|
||||
|
||||
def ensure_fullscreen(self):
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def create_controls(self):
|
||||
# Create a transparent, borderless top-level window for controls
|
||||
self.controls_win = tk.Toplevel(self.root)
|
||||
self.controls_win.overrideredirect(True)
|
||||
self.controls_win.attributes('-topmost', True)
|
||||
self.controls_win.attributes('-alpha', 0.92)
|
||||
self.controls_win.configure(bg='')
|
||||
# Place the window at the bottom right
|
||||
def place_controls():
|
||||
self.controls_win.update_idletasks()
|
||||
w = self.controls_win.winfo_reqwidth()
|
||||
h = self.controls_win.winfo_reqheight()
|
||||
sw = self.root.winfo_screenwidth()
|
||||
sh = self.root.winfo_screenheight()
|
||||
x = sw - w - 30
|
||||
y = sh - h - 30
|
||||
self.controls_win.geometry(f'+{x}+{y}')
|
||||
self.controls_frame = tk.Frame(self.controls_win, bg='#222', bd=2, relief='ridge')
|
||||
self.controls_frame.pack()
|
||||
btn_style = {
|
||||
'bg': '#333',
|
||||
'fg': 'white',
|
||||
'activebackground': '#555',
|
||||
'activeforeground': '#00e6e6',
|
||||
'font': ('Arial', 16, 'bold'),
|
||||
'bd': 0,
|
||||
'highlightthickness': 0,
|
||||
'relief': 'flat',
|
||||
'cursor': 'hand2',
|
||||
'padx': 10,
|
||||
'pady': 6
|
||||
}
|
||||
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style)
|
||||
self.prev_btn.grid(row=0, column=0, padx=4)
|
||||
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style)
|
||||
self.pause_btn.grid(row=0, column=1, padx=4)
|
||||
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style)
|
||||
self.next_btn.grid(row=0, column=2, padx=4)
|
||||
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style)
|
||||
self.settings_btn.grid(row=0, column=3, padx=4)
|
||||
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style)
|
||||
self.exit_btn.grid(row=0, column=4, padx=4)
|
||||
self.exit_btn.config(fg='#ff4d4d')
|
||||
self.controls_win.withdraw()
|
||||
self.controls_win.after(200, place_controls)
|
||||
self.root.bind('<Configure>', lambda e: self.controls_win.after(200, place_controls))
|
||||
|
||||
def hide_mouse(self):
|
||||
self.root.config(cursor='none')
|
||||
if hasattr(self, 'controls_win'):
|
||||
self.controls_win.config(cursor='none')
|
||||
|
||||
def show_mouse(self):
|
||||
self.root.config(cursor='arrow')
|
||||
if hasattr(self, 'controls_win'):
|
||||
self.controls_win.config(cursor='arrow')
|
||||
|
||||
def move_mouse_to_corner(self):
|
||||
try:
|
||||
import pyautogui
|
||||
sw = self.root.winfo_screenwidth()
|
||||
sh = self.root.winfo_screenheight()
|
||||
pyautogui.moveTo(sw-2, sh-2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def show_controls(self):
|
||||
if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists():
|
||||
self.create_controls()
|
||||
self.controls_win.deiconify()
|
||||
self.controls_win.lift()
|
||||
self.show_mouse()
|
||||
self.schedule_hide_controls()
|
||||
|
||||
def hide_controls(self):
|
||||
self.controls_win.withdraw()
|
||||
self.hide_mouse()
|
||||
|
||||
def schedule_hide_controls(self):
|
||||
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
||||
self.root.after_cancel(self.hide_controls_timer)
|
||||
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
|
||||
|
||||
def on_activity(self, event=None):
|
||||
self.show_controls()
|
||||
|
||||
def prev_media(self):
|
||||
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def next_media(self):
|
||||
# If at the last media, update playlist before looping
|
||||
if self.current_index == len(self.playlist) - 1:
|
||||
self.update_playlist_from_server()
|
||||
self.current_index = 0
|
||||
self.show_current_media()
|
||||
else:
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def update_playlist_from_server(self):
|
||||
# Dummy implementation: replace with your actual update logic
|
||||
# For example, call a function to fetch and reload the playlist
|
||||
print("[INFO] Updating playlist from server...")
|
||||
# You can import and call your real update function here
|
||||
# Example: self.playlist = get_latest_playlist()
|
||||
|
||||
def toggle_pause(self):
|
||||
if not self.paused:
|
||||
self.paused = True
|
||||
self.pause_btn.config(text='▶ Resume')
|
||||
self.pause_timer = self.root.after(30000, self.resume_play)
|
||||
else:
|
||||
self.resume_play()
|
||||
|
||||
def resume_play(self):
|
||||
self.paused = False
|
||||
self.pause_btn.config(text='⏸ Pause')
|
||||
if self.pause_timer:
|
||||
self.root.after_cancel(self.pause_timer)
|
||||
self.pause_timer = None
|
||||
|
||||
def play_intro_video(self):
|
||||
intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4')
|
||||
if os.path.exists(intro_path):
|
||||
self.show_video(intro_path, on_end=self.after_intro)
|
||||
else:
|
||||
self.after_intro()
|
||||
|
||||
def after_intro(self):
|
||||
self.show_current_media()
|
||||
self.root.after(100, self.next_media_loop)
|
||||
|
||||
def show_video(self, file_path, on_end=None, duration=None):
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
self.vlc_player.stop()
|
||||
if not hasattr(self, 'video_canvas'):
|
||||
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.label.pack_forget()
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
self.vlc_instance = vlc.Instance('--vout=x11')
|
||||
self.vlc_player = self.vlc_instance.media_player_new()
|
||||
self.vlc_player.set_mrl(file_path)
|
||||
self.vlc_player.set_fullscreen(True)
|
||||
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||
self.vlc_player.play()
|
||||
# Force video to play for the specified duration
|
||||
def finish_video():
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
if duration is not None:
|
||||
self.root.after(int(duration * 1000), finish_video)
|
||||
else:
|
||||
def check_end():
|
||||
if self.vlc_player.get_state() == vlc.State.Ended:
|
||||
finish_video()
|
||||
else:
|
||||
self.root.after(200, check_end)
|
||||
check_end()
|
||||
|
||||
def show_current_media(self):
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
if not self.playlist:
|
||||
self.label.config(text="No media available", fg='white', font=('Arial', 32))
|
||||
return
|
||||
media = self.playlist[self.current_index]
|
||||
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
|
||||
ext = file_path.lower()
|
||||
duration = media.get('duration', None)
|
||||
if ext.endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||
self.show_video(file_path, on_end=self.next_media, duration=duration)
|
||||
elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||
self.show_image_via_vlc(file_path, duration if duration is not None else 10, on_end=self.next_media)
|
||||
else:
|
||||
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
|
||||
|
||||
def show_image_via_vlc(self, file_path, duration, on_end=None):
|
||||
# Use VLC to show image for a set duration
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
self.vlc_player.stop()
|
||||
if not hasattr(self, 'video_canvas'):
|
||||
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.label.pack_forget()
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
self.vlc_instance = vlc.Instance('--vout=x11')
|
||||
self.vlc_player = self.vlc_instance.media_player_new()
|
||||
self.vlc_player.set_mrl(file_path)
|
||||
self.vlc_player.set_fullscreen(True)
|
||||
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||
self.vlc_player.play()
|
||||
# Schedule stop and next after duration
|
||||
def finish_image():
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
self.root.after(int(duration * 1000), finish_image)
|
||||
|
||||
def next_media(self):
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def next_media_loop(self):
|
||||
if not self.playlist or self.paused:
|
||||
self.root.after(1000, self.next_media_loop)
|
||||
return
|
||||
self.show_current_media()
|
||||
|
||||
def exit_app(self):
|
||||
# Signal all threads and flags to stop
|
||||
if hasattr(self, 'stop_event') and self.stop_event:
|
||||
self.stop_event.set()
|
||||
if hasattr(self, 'app_running') and self.app_running:
|
||||
self.app_running[0] = False
|
||||
# Unbind all events to prevent callbacks after destroy
|
||||
try:
|
||||
self.root.unbind('<Motion>')
|
||||
self.root.unbind('<Button-1>')
|
||||
self.root.unbind('<Configure>')
|
||||
except Exception:
|
||||
pass
|
||||
# Attempt to destroy all Toplevel windows before root
|
||||
try:
|
||||
# Withdraw controls_win if it exists
|
||||
if hasattr(self, 'controls_win') and self.controls_win:
|
||||
if self.controls_win.winfo_exists():
|
||||
self.controls_win.withdraw()
|
||||
# Destroy controls_win if it exists (this will also destroy controls_frame)
|
||||
if hasattr(self, 'controls_win') and self.controls_win:
|
||||
if self.controls_win.winfo_exists():
|
||||
self.controls_win.destroy()
|
||||
self.controls_win = None
|
||||
# Fallback: destroy controls_frame if it somehow still exists
|
||||
if hasattr(self, 'controls_frame') and self.controls_frame:
|
||||
if self.controls_frame.winfo_exists():
|
||||
self.controls_frame.destroy()
|
||||
self.controls_frame = None
|
||||
# Fallback: destroy any remaining Toplevels in the app
|
||||
for widget in self.root.winfo_children():
|
||||
if isinstance(widget, tk.Toplevel):
|
||||
try:
|
||||
widget.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[EXIT] Error destroying controls_win/frame/toplevels: {e}")
|
||||
# Destroy any other Toplevels if needed (add here if you have more)
|
||||
try:
|
||||
if self.root.winfo_exists():
|
||||
self.root.destroy()
|
||||
except Exception as e:
|
||||
print(f"[EXIT] Error destroying root: {e}")
|
||||
|
||||
def open_settings(self):
|
||||
if self.paused is not True:
|
||||
self.paused = True
|
||||
self.pause_btn.config(text='▶ Resume')
|
||||
# Explicitly pause VLC video if playing
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
try:
|
||||
self.vlc_player.pause()
|
||||
except Exception:
|
||||
pass
|
||||
# Destroy controls overlay so settings window is always interactive
|
||||
if hasattr(self, 'controls_win') and self.controls_win:
|
||||
self.controls_win.destroy()
|
||||
self.controls_win = None
|
||||
settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py')
|
||||
# Open settings in a new process so it doesn't block the main player
|
||||
proc = subprocess.Popen([sys.executable, settings_path], close_fds=True)
|
||||
# Give the window manager a moment to focus the new window
|
||||
self.root.after(300, lambda: self.root.focus_force())
|
||||
# Wait for the settings window to close, then resume
|
||||
self.root.after(1000, lambda: self.check_settings_closed(proc))
|
||||
|
||||
def check_settings_closed(self, proc):
|
||||
if proc.poll() is not None:
|
||||
# Resume playback and unpause VLC if needed
|
||||
self.resume_play()
|
||||
# Restore and recreate controls overlay
|
||||
self.root.deiconify()
|
||||
self.create_controls()
|
||||
# Re-bind mouse and button events to new controls
|
||||
self.root.bind('<Motion>', self.on_activity)
|
||||
self.root.bind('<Button-1>', self.on_activity)
|
||||
self.show_controls()
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
try:
|
||||
# Only resume if it was paused by us
|
||||
if self.vlc_player.get_state() == vlc.State.Paused:
|
||||
self.vlc_player.play()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self.root.after(1000, lambda: self.check_settings_closed(proc))
|
||||
|
||||
def main_start(self):
|
||||
self.play_intro_video()
|
||||
|
||||
def load_latest_playlist():
|
||||
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
if not files:
|
||||
return []
|
||||
# Sort by version number descending
|
||||
files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True)
|
||||
latest_file = files[0]
|
||||
with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f:
|
||||
data = json.load(f)
|
||||
return data.get('playlist', [])
|
||||
224
signage_player/old_code/player copy.py
Normal file
@@ -0,0 +1,224 @@
|
||||
import os
|
||||
import json
|
||||
import tkinter as tk
|
||||
from PIL import Image, ImageTk
|
||||
import vlc
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
||||
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
|
||||
|
||||
class SimpleTkPlayer:
|
||||
def __init__(self, root, playlist):
|
||||
self.root = root
|
||||
self.playlist = playlist
|
||||
self.current_index = 0
|
||||
self.paused = False
|
||||
self.pause_timer = None
|
||||
self.label = tk.Label(root, bg='black')
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
self.create_controls()
|
||||
self.hide_controls()
|
||||
self.root.bind('<Motion>', self.on_activity)
|
||||
self.root.bind('<Button-1>', self.on_activity)
|
||||
self.root.after(100, self.ensure_fullscreen)
|
||||
|
||||
def ensure_fullscreen(self):
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def create_controls(self):
|
||||
self.controls_frame = tk.Frame(self.root, bg='#222', bd=2, relief='ridge')
|
||||
self.controls_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||
btn_style = {
|
||||
'bg': '#333',
|
||||
'fg': 'white',
|
||||
'activebackground': '#555',
|
||||
'activeforeground': '#00e6e6',
|
||||
'font': ('Arial', 16, 'bold'),
|
||||
'bd': 0,
|
||||
'highlightthickness': 0,
|
||||
'relief': 'flat',
|
||||
'cursor': 'hand2',
|
||||
'padx': 10,
|
||||
'pady': 6
|
||||
}
|
||||
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style)
|
||||
self.prev_btn.grid(row=0, column=0, padx=4)
|
||||
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style)
|
||||
self.pause_btn.grid(row=0, column=1, padx=4)
|
||||
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style)
|
||||
self.next_btn.grid(row=0, column=2, padx=4)
|
||||
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style)
|
||||
self.settings_btn.grid(row=0, column=3, padx=4)
|
||||
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style)
|
||||
self.exit_btn.grid(row=0, column=4, padx=4)
|
||||
self.exit_btn.config(fg='#ff4d4d')
|
||||
|
||||
def show_controls(self):
|
||||
self.controls_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||
self.controls_frame.lift()
|
||||
self.schedule_hide_controls()
|
||||
|
||||
def hide_controls(self):
|
||||
self.controls_frame.place_forget()
|
||||
|
||||
def schedule_hide_controls(self):
|
||||
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
||||
self.root.after_cancel(self.hide_controls_timer)
|
||||
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
|
||||
|
||||
def on_activity(self, event=None):
|
||||
self.show_controls()
|
||||
|
||||
def prev_media(self):
|
||||
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def next_media(self):
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def toggle_pause(self):
|
||||
if not self.paused:
|
||||
self.paused = True
|
||||
self.pause_btn.config(text='▶ Resume')
|
||||
self.pause_timer = self.root.after(30000, self.resume_play)
|
||||
else:
|
||||
self.resume_play()
|
||||
|
||||
def resume_play(self):
|
||||
self.paused = False
|
||||
self.pause_btn.config(text='⏸ Pause')
|
||||
if self.pause_timer:
|
||||
self.root.after_cancel(self.pause_timer)
|
||||
self.pause_timer = None
|
||||
|
||||
def play_intro_video(self):
|
||||
intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4')
|
||||
if os.path.exists(intro_path):
|
||||
self.show_video(intro_path, on_end=self.after_intro)
|
||||
else:
|
||||
self.after_intro()
|
||||
|
||||
def after_intro(self):
|
||||
self.show_current_media()
|
||||
self.root.after(100, self.next_media_loop)
|
||||
|
||||
def show_video(self, file_path, on_end=None):
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
self.vlc_player.stop()
|
||||
if not hasattr(self, 'video_canvas'):
|
||||
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.label.pack_forget()
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
self.vlc_instance = vlc.Instance()
|
||||
self.vlc_player = self.vlc_instance.media_player_new()
|
||||
self.vlc_player.set_mrl(file_path)
|
||||
self.vlc_player.set_fullscreen(True)
|
||||
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||
self.vlc_player.play()
|
||||
def check_end():
|
||||
if self.vlc_player.get_state() == vlc.State.Ended:
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
else:
|
||||
self.root.after(200, check_end)
|
||||
check_end()
|
||||
|
||||
def show_current_media(self):
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
if not self.playlist:
|
||||
self.label.config(text="No media available", fg='white', font=('Arial', 32))
|
||||
return
|
||||
media = self.playlist[self.current_index]
|
||||
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
|
||||
if file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||
self.show_video(file_path, on_end=self.next_media)
|
||||
elif file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||
try:
|
||||
img = Image.open(file_path)
|
||||
# Fit to screen without crop or stretch
|
||||
screen_w = self.root.winfo_screenwidth()
|
||||
screen_h = self.root.winfo_screenheight()
|
||||
img_w, img_h = img.size
|
||||
scale = min(screen_w / img_w, screen_h / img_h)
|
||||
new_w = int(img_w * scale)
|
||||
new_h = int(img_h * scale)
|
||||
img = img.resize((new_w, new_h), Image.LANCZOS)
|
||||
bg = Image.new('RGB', (screen_w, screen_h), 'black')
|
||||
x = (screen_w - new_w) // 2
|
||||
y = (screen_h - new_h) // 2
|
||||
bg.paste(img, (x, y))
|
||||
photo = ImageTk.PhotoImage(bg)
|
||||
self.label.config(image=photo, text='')
|
||||
self.label.image = photo
|
||||
except Exception as e:
|
||||
self.label.config(text=f"Image error: {e}", fg='red')
|
||||
else:
|
||||
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
|
||||
|
||||
def next_media_loop(self):
|
||||
if not self.playlist or self.paused:
|
||||
self.root.after(1000, self.next_media_loop)
|
||||
return
|
||||
duration = self.playlist[self.current_index].get('duration', 10)
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
self.root.after(duration * 1000, self.next_media_loop)
|
||||
|
||||
def exit_app(self):
|
||||
# Signal the update thread to stop if stop_event is present
|
||||
if hasattr(self, 'stop_event') and self.stop_event:
|
||||
self.stop_event.set()
|
||||
self.root.destroy()
|
||||
|
||||
def open_settings(self):
|
||||
if self.paused is not True:
|
||||
self.paused = True
|
||||
self.pause_btn.config(text='▶ Resume')
|
||||
settings_win = tk.Toplevel(self.root)
|
||||
settings_win.title('Settings')
|
||||
settings_win.geometry('400x300+100+100')
|
||||
settings_win.transient(self.root)
|
||||
settings_win.grab_set()
|
||||
tk.Label(settings_win, text='Settings', font=('Arial', 18)).pack(pady=10)
|
||||
# Example setting: close button
|
||||
tk.Button(settings_win, text='Close', command=settings_win.destroy).pack(pady=20)
|
||||
def on_close():
|
||||
settings_win.grab_release()
|
||||
settings_win.destroy()
|
||||
self.resume_play()
|
||||
settings_win.protocol('WM_DELETE_WINDOW', on_close)
|
||||
settings_win.bind('<Destroy>', lambda e: self.resume_play() if not settings_win.winfo_exists() else None)
|
||||
|
||||
def main_start(self):
|
||||
self.play_intro_video()
|
||||
|
||||
def load_latest_playlist():
|
||||
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
if not files:
|
||||
return []
|
||||
files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True)
|
||||
with open(os.path.join(PLAYLIST_DIR, files[0]), 'r') as f:
|
||||
data = json.load(f)
|
||||
return data.get('playlist', [])
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
root.title("Simple Tkinter Player")
|
||||
root.configure(bg='black')
|
||||
root.attributes('-fullscreen', True)
|
||||
playlist = load_latest_playlist()
|
||||
player = SimpleTkPlayer(root, playlist)
|
||||
player.main_start()
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
423
signage_player/old_code/playerjoi 4 sept.py
Normal file
@@ -0,0 +1,423 @@
|
||||
import os
|
||||
import json
|
||||
import tkinter as tk
|
||||
import vlc
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
|
||||
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
|
||||
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
|
||||
|
||||
class SimpleTkPlayer:
|
||||
def __init__(self, root, playlist):
|
||||
self.root = root
|
||||
self.playlist = playlist
|
||||
self.current_index = 0
|
||||
self.paused = False
|
||||
self.pause_timer = None
|
||||
self.label = tk.Label(root, bg='black')
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
self.create_controls()
|
||||
self.hide_controls()
|
||||
self.root.bind('<Motion>', self.on_activity)
|
||||
self.root.bind('<Button-1>', self.on_activity)
|
||||
self.root.after(100, self.ensure_fullscreen)
|
||||
self.root.after(200, self.hide_mouse)
|
||||
self.root.after(300, self.move_mouse_to_corner)
|
||||
self.root.protocol('WM_DELETE_WINDOW', self.exit_app)
|
||||
|
||||
def ensure_fullscreen(self):
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def create_controls(self):
|
||||
# Create a transparent, borderless top-level window for controls
|
||||
self.controls_win = tk.Toplevel(self.root)
|
||||
self.controls_win.overrideredirect(True)
|
||||
self.controls_win.attributes('-topmost', True)
|
||||
self.controls_win.attributes('-alpha', 0.92)
|
||||
self.controls_win.configure(bg='')
|
||||
# Place the window at the bottom right
|
||||
def place_controls():
|
||||
self.controls_win.update_idletasks()
|
||||
w = self.controls_win.winfo_reqwidth()
|
||||
h = self.controls_win.winfo_reqheight()
|
||||
sw = self.root.winfo_screenwidth()
|
||||
sh = self.root.winfo_screenheight()
|
||||
x = sw - w - 30
|
||||
y = sh - h - 30
|
||||
self.controls_win.geometry(f'+{x}+{y}')
|
||||
self.controls_frame = tk.Frame(self.controls_win, bg='#222', bd=2, relief='ridge')
|
||||
self.controls_frame.pack()
|
||||
btn_style = {
|
||||
'bg': '#333',
|
||||
'fg': 'white',
|
||||
'activebackground': '#555',
|
||||
'activeforeground': '#00e6e6',
|
||||
'font': ('Arial', 16, 'bold'),
|
||||
'bd': 0,
|
||||
'highlightthickness': 0,
|
||||
'relief': 'flat',
|
||||
'cursor': 'hand2',
|
||||
'padx': 10,
|
||||
'pady': 6
|
||||
}
|
||||
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style)
|
||||
self.prev_btn.grid(row=0, column=0, padx=4)
|
||||
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style)
|
||||
self.pause_btn.grid(row=0, column=1, padx=4)
|
||||
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style)
|
||||
self.next_btn.grid(row=0, column=2, padx=4)
|
||||
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style)
|
||||
self.settings_btn.grid(row=0, column=3, padx=4)
|
||||
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style)
|
||||
self.exit_btn.grid(row=0, column=4, padx=4)
|
||||
self.exit_btn.config(fg='#ff4d4d')
|
||||
self.controls_win.withdraw()
|
||||
self.controls_win.after(200, place_controls)
|
||||
self.root.bind('<Configure>', lambda e: self.controls_win.after(200, place_controls))
|
||||
|
||||
def hide_mouse(self):
|
||||
self.root.config(cursor='none')
|
||||
if hasattr(self, 'controls_win'):
|
||||
self.controls_win.config(cursor='none')
|
||||
|
||||
def show_mouse(self):
|
||||
self.root.config(cursor='arrow')
|
||||
if hasattr(self, 'controls_win'):
|
||||
self.controls_win.config(cursor='arrow')
|
||||
|
||||
def move_mouse_to_corner(self):
|
||||
try:
|
||||
import pyautogui
|
||||
sw = self.root.winfo_screenwidth()
|
||||
sh = self.root.winfo_screenheight()
|
||||
pyautogui.moveTo(sw-2, sh-2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def show_controls(self):
|
||||
if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists():
|
||||
self.create_controls()
|
||||
self.controls_win.deiconify()
|
||||
self.controls_win.lift()
|
||||
self.show_mouse()
|
||||
self.schedule_hide_controls()
|
||||
|
||||
def hide_controls(self):
|
||||
self.controls_win.withdraw()
|
||||
self.hide_mouse()
|
||||
|
||||
def schedule_hide_controls(self):
|
||||
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
||||
self.root.after_cancel(self.hide_controls_timer)
|
||||
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
|
||||
|
||||
def on_activity(self, event=None):
|
||||
self.show_controls()
|
||||
|
||||
def prev_media(self):
|
||||
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def next_media(self):
|
||||
# If at the last media, update playlist before looping
|
||||
if self.current_index == len(self.playlist) - 1:
|
||||
self.update_playlist_from_server()
|
||||
self.current_index = 0
|
||||
self.show_current_media()
|
||||
else:
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def update_playlist_from_server(self):
|
||||
# Dummy implementation: replace with your actual update logic
|
||||
# For example, call a function to fetch and reload the playlist
|
||||
print("[INFO] Updating playlist from server...")
|
||||
# You can import and call your real update function here
|
||||
# Example: self.playlist = get_latest_playlist()
|
||||
|
||||
def toggle_pause(self):
|
||||
if not self.paused:
|
||||
self.paused = True
|
||||
self.pause_btn.config(text='▶ Resume')
|
||||
self.pause_timer = self.root.after(30000, self.resume_play)
|
||||
else:
|
||||
self.resume_play()
|
||||
|
||||
def resume_play(self):
|
||||
self.paused = False
|
||||
self.pause_btn.config(text='⏸ Pause')
|
||||
if self.pause_timer:
|
||||
self.root.after_cancel(self.pause_timer)
|
||||
self.pause_timer = None
|
||||
|
||||
def play_intro_video(self):
|
||||
intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4')
|
||||
if os.path.exists(intro_path):
|
||||
self.show_video(intro_path, on_end=self.after_intro)
|
||||
else:
|
||||
self.after_intro()
|
||||
|
||||
def after_intro(self):
|
||||
self.show_current_media()
|
||||
self.root.after(100, self.next_media_loop)
|
||||
|
||||
def show_video(self, file_path, on_end=None, duration=None):
|
||||
try:
|
||||
print(f"[PLAYER] Attempting to play video: {file_path}")
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
self.vlc_player.stop()
|
||||
if not hasattr(self, 'video_canvas'):
|
||||
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.label.pack_forget()
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
self.vlc_instance = vlc.Instance('--vout=x11')
|
||||
self.vlc_player = self.vlc_instance.media_player_new()
|
||||
self.vlc_player.set_mrl(file_path)
|
||||
self.vlc_player.set_fullscreen(True)
|
||||
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||
self.vlc_player.play()
|
||||
# Watchdog timer: fallback if video doesn't end
|
||||
def watchdog():
|
||||
print(f"[WATCHDOG] Video watchdog triggered for {file_path}")
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
max_duration = duration if duration is not None else 60 # fallback max 60s
|
||||
self.video_watchdog = self.root.after(int(max_duration * 1200), watchdog)
|
||||
def finish_video():
|
||||
if hasattr(self, 'video_watchdog'):
|
||||
self.root.after_cancel(self.video_watchdog)
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
if duration is not None:
|
||||
self.root.after(int(duration * 1000), finish_video)
|
||||
else:
|
||||
def check_end():
|
||||
try:
|
||||
if self.vlc_player.get_state() == vlc.State.Ended:
|
||||
finish_video()
|
||||
elif self.vlc_player.get_state() == vlc.State.Error:
|
||||
print(f"[VLC] Error state detected for {file_path}")
|
||||
finish_video()
|
||||
else:
|
||||
self.root.after(200, check_end)
|
||||
except Exception as e:
|
||||
print(f"[VLC] Exception in check_end: {e}")
|
||||
finish_video()
|
||||
check_end()
|
||||
except Exception as e:
|
||||
print(f"[VLC] Error playing video {file_path}: {e}")
|
||||
if on_end:
|
||||
on_end()
|
||||
|
||||
def show_current_media(self):
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
if not self.playlist:
|
||||
print("[PLAYER] Playlist is empty. No media to show.")
|
||||
self.label.config(text="No media available", fg='white', font=('Arial', 32))
|
||||
# Try to reload playlist after 10 seconds
|
||||
self.root.after(10000, self.reload_playlist_and_continue)
|
||||
return
|
||||
media = self.playlist[self.current_index]
|
||||
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
|
||||
ext = file_path.lower()
|
||||
duration = media.get('duration', None)
|
||||
if not os.path.isfile(file_path):
|
||||
print(f"[PLAYER] File missing: {file_path}. Skipping to next.")
|
||||
self.next_media()
|
||||
return
|
||||
if ext.endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||
self.show_video(file_path, on_end=self.next_media, duration=duration)
|
||||
elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||
self.show_image_via_vlc(file_path, duration if duration is not None else 10, on_end=self.next_media)
|
||||
else:
|
||||
print(f"[PLAYER] Unsupported file type: {media['file_name']}")
|
||||
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
|
||||
self.root.after(2000, self.next_media)
|
||||
|
||||
def reload_playlist_and_continue(self):
|
||||
print("[PLAYER] Attempting to reload playlist...")
|
||||
new_playlist = load_latest_playlist()
|
||||
if new_playlist:
|
||||
self.playlist = new_playlist
|
||||
self.current_index = 0
|
||||
print("[PLAYER] Playlist reloaded. Continuing playback.")
|
||||
self.show_current_media()
|
||||
else:
|
||||
print("[PLAYER] Still no playlist. Will retry.")
|
||||
self.root.after(10000, self.reload_playlist_and_continue)
|
||||
|
||||
def show_image_via_vlc(self, file_path, duration, on_end=None):
|
||||
try:
|
||||
print(f"[PLAYER] Attempting to show image: {file_path}")
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
self.vlc_player.stop()
|
||||
if not hasattr(self, 'video_canvas'):
|
||||
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.label.pack_forget()
|
||||
self.video_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.update_idletasks()
|
||||
self.vlc_instance = vlc.Instance('--vout=x11')
|
||||
self.vlc_player = self.vlc_instance.media_player_new()
|
||||
self.vlc_player.set_mrl(file_path)
|
||||
self.vlc_player.set_fullscreen(True)
|
||||
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
|
||||
self.vlc_player.play()
|
||||
# Watchdog timer: fallback if image doesn't advance
|
||||
def watchdog():
|
||||
print(f"[WATCHDOG] Image watchdog triggered for {file_path}")
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
self.image_watchdog = self.root.after(int(duration * 1200), watchdog)
|
||||
def finish_image():
|
||||
if hasattr(self, 'image_watchdog'):
|
||||
self.root.after_cancel(self.image_watchdog)
|
||||
self.vlc_player.stop()
|
||||
self.video_canvas.pack_forget()
|
||||
self.label.pack(fill=tk.BOTH, expand=True)
|
||||
if on_end:
|
||||
on_end()
|
||||
self.root.after(int(duration * 1000), finish_image)
|
||||
except Exception as e:
|
||||
print(f"[VLC] Error showing image {file_path}: {e}")
|
||||
if on_end:
|
||||
on_end()
|
||||
|
||||
def next_media(self):
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
self.show_current_media()
|
||||
|
||||
def next_media_loop(self):
|
||||
if not self.playlist or self.paused:
|
||||
self.root.after(1000, self.next_media_loop)
|
||||
return
|
||||
self.show_current_media()
|
||||
|
||||
def exit_app(self):
|
||||
# Signal all threads and flags to stop
|
||||
if hasattr(self, 'stop_event') and self.stop_event:
|
||||
self.stop_event.set()
|
||||
if hasattr(self, 'app_running') and self.app_running:
|
||||
self.app_running[0] = False
|
||||
# Unbind all events to prevent callbacks after destroy
|
||||
try:
|
||||
self.root.unbind('<Motion>')
|
||||
self.root.unbind('<Button-1>')
|
||||
self.root.unbind('<Configure>')
|
||||
except Exception:
|
||||
pass
|
||||
# Attempt to destroy all Toplevel windows before root
|
||||
try:
|
||||
# Withdraw controls_win if it exists
|
||||
if hasattr(self, 'controls_win') and self.controls_win:
|
||||
if self.controls_win.winfo_exists():
|
||||
self.controls_win.withdraw()
|
||||
# Destroy controls_win if it exists (this will also destroy controls_frame)
|
||||
if hasattr(self, 'controls_win') and self.controls_win:
|
||||
if self.controls_win.winfo_exists():
|
||||
self.controls_win.destroy()
|
||||
self.controls_win = None
|
||||
# Fallback: destroy controls_frame if it somehow still exists
|
||||
if hasattr(self, 'controls_frame') and self.controls_frame:
|
||||
if self.controls_frame.winfo_exists():
|
||||
self.controls_frame.destroy()
|
||||
self.controls_frame = None
|
||||
# Fallback: destroy any remaining Toplevels in the app
|
||||
for widget in self.root.winfo_children():
|
||||
if isinstance(widget, tk.Toplevel):
|
||||
try:
|
||||
widget.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[EXIT] Error destroying controls_win/frame/toplevels: {e}")
|
||||
# Destroy any other Toplevels if needed (add here if you have more)
|
||||
try:
|
||||
if self.root.winfo_exists():
|
||||
self.root.destroy()
|
||||
except Exception as e:
|
||||
print(f"[EXIT] Error destroying root: {e}")
|
||||
|
||||
def open_settings(self):
|
||||
if self.paused is not True:
|
||||
self.paused = True
|
||||
self.pause_btn.config(text='▶ Resume')
|
||||
# Explicitly pause VLC video if playing
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
try:
|
||||
self.vlc_player.pause()
|
||||
except Exception:
|
||||
pass
|
||||
# Destroy controls overlay so settings window is always interactive
|
||||
if hasattr(self, 'controls_win') and self.controls_win:
|
||||
self.controls_win.destroy()
|
||||
self.controls_win = None
|
||||
settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py')
|
||||
# Open settings in a new process so it doesn't block the main player
|
||||
proc = subprocess.Popen([sys.executable, settings_path], close_fds=True)
|
||||
# Give the window manager a moment to focus the new window
|
||||
self.root.after(300, lambda: self.root.focus_force())
|
||||
# Wait for the settings window to close, then resume
|
||||
self.root.after(1000, lambda: self.check_settings_closed(proc))
|
||||
|
||||
def check_settings_closed(self, proc):
|
||||
if proc.poll() is not None:
|
||||
# Resume playback and unpause VLC if needed
|
||||
self.resume_play()
|
||||
# Restore and recreate controls overlay
|
||||
self.root.deiconify()
|
||||
self.create_controls()
|
||||
# Re-bind mouse and button events to new controls
|
||||
self.root.bind('<Motion>', self.on_activity)
|
||||
self.root.bind('<Button-1>', self.on_activity)
|
||||
self.show_controls()
|
||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||
try:
|
||||
# Only resume if it was paused by us
|
||||
if self.vlc_player.get_state() == vlc.State.Paused:
|
||||
self.vlc_player.play()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self.root.after(1000, lambda: self.check_settings_closed(proc))
|
||||
|
||||
def main_start(self):
|
||||
self.play_intro_video()
|
||||
|
||||
def load_latest_playlist():
|
||||
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
if not files:
|
||||
return []
|
||||
# Sort by version number descending
|
||||
files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True)
|
||||
latest_file = files[0]
|
||||
with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f:
|
||||
data = json.load(f)
|
||||
playlist = data.get('playlist', [])
|
||||
# Validate playlist: skip missing or unsupported files
|
||||
valid_exts = ('.mp4', '.avi', '.mov', '.mkv', '.jpg', '.jpeg', '.png', '.bmp', '.gif')
|
||||
valid_playlist = []
|
||||
for item in playlist:
|
||||
file_path = os.path.join(MEDIA_DATA_PATH, item.get('file_name', ''))
|
||||
if os.path.isfile(file_path) and file_path.lower().endswith(valid_exts):
|
||||
valid_playlist.append(item)
|
||||
else:
|
||||
print(f"[PLAYLIST] Skipping missing or unsupported file: {item.get('file_name')}")
|
||||
return valid_playlist
|
||||
1220
signage_player/player.py
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
20
signage_player/static_data/playlist/server_playlist_v8.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "one-piece-season-2-5120x2880-23673.jpg",
|
||||
"url": "media/one-piece-season-2-5120x2880-23673.jpg",
|
||||
"duration": 30
|
||||
},
|
||||
{
|
||||
"file_name": "call-of-duty-black-3840x2160-23674.jpg",
|
||||
"url": "media/call-of-duty-black-3840x2160-23674.jpg",
|
||||
"duration": 30
|
||||
},
|
||||
{
|
||||
"file_name": "big-buck-bunny-1080p-60fps-30sec.mp4",
|
||||
"url": "media/big-buck-bunny-1080p-60fps-30sec.mp4",
|
||||
"duration": 30
|
||||
}
|
||||
],
|
||||
"version": 8
|
||||
}
|
||||
111
src/README.md
@@ -1,111 +0,0 @@
|
||||
# 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
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/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 "$(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 --system-site-packages venv
|
||||
echo "Virtual environment created with system site packages access."
|
||||
else
|
||||
echo "Removing existing virtual environment and creating new one with system site packages..."
|
||||
rm -rf venv
|
||||
python3 -m venv --system-site-packages venv
|
||||
echo "Virtual environment recreated with system site packages access."
|
||||
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..."
|
||||
OFFLINE_PACKAGES_DIR="$PROJECT_ROOT/src/offline_packages"
|
||||
if [ -d "$OFFLINE_PACKAGES_DIR" ]; then
|
||||
pip install --upgrade pip
|
||||
pip install --no-index --find-links "$OFFLINE_PACKAGES_DIR" \
|
||||
requests pillow pygame certifi charset_normalizer idna urllib3
|
||||
echo "Python packages installed successfully."
|
||||
else
|
||||
echo "ERROR: Offline packages directory not found at: $OFFLINE_PACKAGES_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy shared modules to tkinter app
|
||||
echo "Step 5: Setting up shared modules..."
|
||||
SHARED_MODULES_DIR="$PROJECT_ROOT/src/shared_modules"
|
||||
TKINTER_APP_DIR="$PROJECT_ROOT/tkinter_app/src"
|
||||
if [ -d "$SHARED_MODULES_DIR" ]; then
|
||||
cp "$SHARED_MODULES_DIR"/*.py "$TKINTER_APP_DIR"/ 2>/dev/null || echo "Shared modules already in place."
|
||||
echo "Shared modules configured."
|
||||
else
|
||||
echo "Warning: Shared modules directory not found at: $SHARED_MODULES_DIR"
|
||||
fi
|
||||
|
||||
# Create necessary directories
|
||||
echo "Step 6: Creating application directories..."
|
||||
mkdir -p "$PROJECT_ROOT/tkinter_app/resources/static/resurse"
|
||||
mkdir -p "$PROJECT_ROOT/tkinter_app/src/static/resurse"
|
||||
|
||||
# Set permissions
|
||||
echo "Step 7: Setting permissions..."
|
||||
chmod +x "$PROJECT_ROOT/run_tkinter_debug.sh" 2>/dev/null || true
|
||||
chmod +x "$PROJECT_ROOT/install_tkinter.sh" 2>/dev/null || true
|
||||
chmod +x "$PROJECT_ROOT/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 ""
|
||||
@@ -1,27 +0,0 @@
|
||||
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)
|
||||
@@ -1,196 +0,0 @@
|
||||
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}")
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,143 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify window centering functionality
|
||||
"""
|
||||
import tkinter as tk
|
||||
import sys
|
||||
import os
|
||||
sys.path.append('tkinter_app/src')
|
||||
from tkinter_simple_player import SettingsWindow, SimpleMediaPlayerApp
|
||||
|
||||
def test_settings_centering():
|
||||
"""Test settings window centering"""
|
||||
root = tk.Tk()
|
||||
root.withdraw() # Hide main window
|
||||
|
||||
# Create a mock app object
|
||||
class MockApp:
|
||||
def __init__(self):
|
||||
self.playlist = []
|
||||
self.current_index = 0
|
||||
|
||||
def play_current_media(self):
|
||||
print('play_current_media called')
|
||||
|
||||
app = MockApp()
|
||||
|
||||
# Test settings window centering
|
||||
try:
|
||||
print("Testing settings window centering...")
|
||||
settings = SettingsWindow(root, app)
|
||||
|
||||
# Get screen dimensions
|
||||
screen_width = settings.window.winfo_screenwidth()
|
||||
screen_height = settings.window.winfo_screenheight()
|
||||
|
||||
# Get window position
|
||||
settings.window.update_idletasks()
|
||||
window_x = settings.window.winfo_x()
|
||||
window_y = settings.window.winfo_y()
|
||||
window_width = 900
|
||||
window_height = 700
|
||||
|
||||
# Calculate expected center position
|
||||
expected_x = (screen_width - window_width) // 2
|
||||
expected_y = (screen_height - window_height) // 2
|
||||
|
||||
print(f"Screen size: {screen_width}x{screen_height}")
|
||||
print(f"Window position: {window_x}, {window_y}")
|
||||
print(f"Expected center: {expected_x}, {expected_y}")
|
||||
print(f"Window size: {window_width}x{window_height}")
|
||||
|
||||
# Check if window is roughly centered (allow some margin for window decorations)
|
||||
margin = 50
|
||||
is_centered_x = abs(window_x - expected_x) <= margin
|
||||
is_centered_y = abs(window_y - expected_y) <= margin
|
||||
|
||||
if is_centered_x and is_centered_y:
|
||||
print("✅ Settings window is properly centered!")
|
||||
else:
|
||||
print("❌ Settings window centering needs adjustment")
|
||||
|
||||
# Keep window open for 3 seconds to visually verify
|
||||
root.after(3000, root.quit)
|
||||
root.mainloop()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error testing settings window: {e}")
|
||||
|
||||
def test_exit_dialog_centering():
|
||||
"""Test exit dialog centering"""
|
||||
print("\nTesting exit dialog centering...")
|
||||
|
||||
# Create a simple test for the centering function
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
# Create a test dialog
|
||||
dialog = tk.Toplevel(root)
|
||||
dialog.title("Test Exit Dialog")
|
||||
dialog.geometry("400x200")
|
||||
dialog.configure(bg='#2d2d2d')
|
||||
dialog.resizable(False, False)
|
||||
|
||||
# Test the centering logic
|
||||
dialog.update_idletasks()
|
||||
screen_width = dialog.winfo_screenwidth()
|
||||
screen_height = dialog.winfo_screenheight()
|
||||
dialog_width = 400
|
||||
dialog_height = 200
|
||||
|
||||
# Calculate center position
|
||||
center_x = int((screen_width - dialog_width) / 2)
|
||||
center_y = int((screen_height - dialog_height) / 2)
|
||||
|
||||
# Ensure the dialog doesn't go off-screen
|
||||
center_x = max(0, min(center_x, screen_width - dialog_width))
|
||||
center_y = max(0, min(center_y, screen_height - dialog_height))
|
||||
|
||||
dialog.geometry(f"{dialog_width}x{dialog_height}+{center_x}+{center_y}")
|
||||
dialog.lift()
|
||||
|
||||
# Add test content
|
||||
tk.Label(dialog, text="🎬 Test Exit Dialog",
|
||||
font=('Arial', 16, 'bold'),
|
||||
fg='white', bg='#2d2d2d').pack(pady=20)
|
||||
|
||||
tk.Label(dialog, text="This dialog should be centered on screen",
|
||||
font=('Arial', 12),
|
||||
fg='white', bg='#2d2d2d').pack(pady=10)
|
||||
|
||||
# Get actual position
|
||||
dialog.update_idletasks()
|
||||
actual_x = dialog.winfo_x()
|
||||
actual_y = dialog.winfo_y()
|
||||
|
||||
print(f"Screen size: {screen_width}x{screen_height}")
|
||||
print(f"Dialog position: {actual_x}, {actual_y}")
|
||||
print(f"Expected center: {center_x}, {center_y}")
|
||||
|
||||
# Check centering
|
||||
margin = 50
|
||||
is_centered_x = abs(actual_x - center_x) <= margin
|
||||
is_centered_y = abs(actual_y - center_y) <= margin
|
||||
|
||||
if is_centered_x and is_centered_y:
|
||||
print("✅ Exit dialog is properly centered!")
|
||||
else:
|
||||
print("❌ Exit dialog centering needs adjustment")
|
||||
|
||||
# Close dialog after 3 seconds
|
||||
root.after(3000, root.quit)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🧪 Testing Window Centering Functionality")
|
||||
print("=" * 50)
|
||||
|
||||
test_settings_centering()
|
||||
test_exit_dialog_centering()
|
||||
|
||||
print("\n✅ Centering tests completed!")
|
||||
print("\nThe windows should appear centered on your screen regardless of resolution.")
|
||||
print("This works for any screen size: 1024x768, 1920x1080, 4K, etc.")
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify image display functionality
|
||||
"""
|
||||
import tkinter as tk
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
|
||||
def test_image_display():
|
||||
# Create a simple tkinter window
|
||||
root = tk.Tk()
|
||||
root.title("Image Display Test")
|
||||
root.geometry("800x600")
|
||||
root.configure(bg='black')
|
||||
|
||||
# Create image label
|
||||
image_label = tk.Label(root, bg='black')
|
||||
image_label.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Test image path
|
||||
test_image = "/home/pi/Desktop/signage-player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg"
|
||||
|
||||
try:
|
||||
if os.path.exists(test_image):
|
||||
print(f"Loading image: {test_image}")
|
||||
|
||||
# Load and display image
|
||||
img = Image.open(test_image)
|
||||
img.thumbnail((800, 600), Image.LANCZOS)
|
||||
photo = ImageTk.PhotoImage(img)
|
||||
|
||||
image_label.config(image=photo)
|
||||
image_label.image = photo # Keep reference
|
||||
|
||||
print(f"Image loaded successfully: {img.size}")
|
||||
|
||||
# Close after 3 seconds
|
||||
root.after(3000, root.quit)
|
||||
|
||||
else:
|
||||
print(f"Image file not found: {test_image}")
|
||||
image_label.config(text="Image file not found", fg='white')
|
||||
root.after(2000, root.quit)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading image: {e}")
|
||||
image_label.config(text=f"Error: {e}", fg='red')
|
||||
root.after(2000, root.quit)
|
||||
|
||||
root.mainloop()
|
||||
print("Image display test completed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_image_display()
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
print("Testing imports...")
|
||||
|
||||
try:
|
||||
import tkinter as tk
|
||||
print("✓ tkinter imported successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ tkinter import failed: {e}")
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
print("✓ PIL and ImageTk imported successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ PIL import failed: {e}")
|
||||
|
||||
try:
|
||||
from virtual_keyboard import VirtualKeyboard
|
||||
print("✓ Virtual keyboard imported successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ Virtual keyboard import failed: {e}")
|
||||
|
||||
try:
|
||||
from python_functions import load_local_playlist
|
||||
print("✓ Python functions imported successfully")
|
||||
|
||||
# Test loading playlist
|
||||
playlist_data = load_local_playlist()
|
||||
playlist = playlist_data.get('playlist', [])
|
||||
print(f"✓ Local playlist loaded: {len(playlist)} items")
|
||||
|
||||
for i, item in enumerate(playlist):
|
||||
print(f" {i+1}. {item.get('file_name', 'Unknown')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Python functions import/execution failed: {e}")
|
||||
|
||||
print("Import test completed")
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for full-screen image scaling functionality
|
||||
"""
|
||||
import tkinter as tk
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
|
||||
def test_scaling_modes():
|
||||
"""Test different scaling modes for images"""
|
||||
|
||||
def scale_image_to_screen(img, screen_width, screen_height, mode='fit'):
|
||||
"""Test scaling function"""
|
||||
img_width, img_height = img.size
|
||||
|
||||
if mode == 'stretch':
|
||||
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
|
||||
|
||||
elif mode == 'fill':
|
||||
screen_ratio = screen_width / screen_height
|
||||
img_ratio = img_width / img_height
|
||||
|
||||
if img_ratio > screen_ratio:
|
||||
new_height = screen_height
|
||||
new_width = int(screen_height * img_ratio)
|
||||
x_offset = (screen_width - new_width) // 2
|
||||
y_offset = 0
|
||||
else:
|
||||
new_width = screen_width
|
||||
new_height = int(screen_width / img_ratio)
|
||||
x_offset = 0
|
||||
y_offset = (screen_height - new_height) // 2
|
||||
|
||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||
|
||||
if new_width > screen_width:
|
||||
crop_x = (new_width - screen_width) // 2
|
||||
img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height))
|
||||
x_offset = 0
|
||||
if new_height > screen_height:
|
||||
crop_y = (new_height - screen_height) // 2
|
||||
img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height))
|
||||
y_offset = 0
|
||||
|
||||
final_img.paste(img_resized, (x_offset, y_offset))
|
||||
return final_img, (x_offset, y_offset)
|
||||
|
||||
else: # fit mode
|
||||
screen_ratio = screen_width / screen_height
|
||||
img_ratio = img_width / img_height
|
||||
|
||||
if img_ratio > screen_ratio:
|
||||
new_width = screen_width
|
||||
new_height = int(screen_width / img_ratio)
|
||||
else:
|
||||
new_height = screen_height
|
||||
new_width = int(screen_height * img_ratio)
|
||||
|
||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||
x_offset = (screen_width - new_width) // 2
|
||||
y_offset = (screen_height - new_height) // 2
|
||||
final_img.paste(img_resized, (x_offset, y_offset))
|
||||
|
||||
return final_img, (x_offset, y_offset)
|
||||
|
||||
# Test image path
|
||||
test_image = "/home/pi/Desktop/signage-player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg"
|
||||
|
||||
if not os.path.exists(test_image):
|
||||
print(f"Test image not found: {test_image}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Load test image
|
||||
img = Image.open(test_image)
|
||||
original_size = img.size
|
||||
screen_width, screen_height = 800, 600
|
||||
|
||||
print(f"Testing scaling modes for image: {original_size}")
|
||||
print(f"Target screen size: {screen_width}x{screen_height}")
|
||||
|
||||
# Test each scaling mode
|
||||
modes = ['fit', 'fill', 'stretch']
|
||||
|
||||
for mode in modes:
|
||||
final_img, offset = scale_image_to_screen(img, screen_width, screen_height, mode)
|
||||
print(f"{mode.upper()} mode: Final size: {final_img.size}, Offset: {offset}")
|
||||
|
||||
print("✅ All scaling modes tested successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error testing scaling: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_scaling_modes()
|
||||
230
test_touch.py
@@ -1,230 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Touch Display Test - Test the touch-optimized interface with virtual keyboard
|
||||
"""
|
||||
import tkinter as tk
|
||||
import sys
|
||||
import os
|
||||
sys.path.append('tkinter_app/src')
|
||||
|
||||
def test_touch_interface():
|
||||
"""Test the touch-optimized settings interface"""
|
||||
try:
|
||||
from tkinter_simple_player import SettingsWindow
|
||||
|
||||
# Create main window
|
||||
root = tk.Tk()
|
||||
root.title("🎮 Touch Display Test")
|
||||
root.geometry("1024x768")
|
||||
root.configure(bg='#2c3e50')
|
||||
|
||||
# Create welcome screen
|
||||
welcome_frame = tk.Frame(root, bg='#2c3e50', padx=40, pady=40)
|
||||
welcome_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Title
|
||||
title_label = tk.Label(welcome_frame,
|
||||
text="🎬 Touch Display Digital Signage",
|
||||
font=('Segoe UI', 24, 'bold'),
|
||||
fg='white', bg='#2c3e50')
|
||||
title_label.pack(pady=30)
|
||||
|
||||
# Description
|
||||
desc_text = (
|
||||
"Touch-Optimized Features:\n\n"
|
||||
"📱 Virtual On-Screen Keyboard\n"
|
||||
"🎯 Larger Touch-Friendly Buttons\n"
|
||||
"⌨️ Auto-Show Keyboard on Input Focus\n"
|
||||
"👆 Enhanced Touch Feedback\n"
|
||||
"🎨 Dark Theme Optimized for Displays\n\n"
|
||||
"Click the button below to test the settings interface:"
|
||||
)
|
||||
|
||||
desc_label = tk.Label(welcome_frame, text=desc_text,
|
||||
font=('Segoe UI', 14),
|
||||
fg='#ecf0f1', bg='#2c3e50',
|
||||
justify=tk.CENTER)
|
||||
desc_label.pack(pady=20)
|
||||
|
||||
# Create mock app for testing
|
||||
class MockApp:
|
||||
def __init__(self):
|
||||
self.playlist = []
|
||||
self.current_index = 0
|
||||
|
||||
def play_current_media(self):
|
||||
print("Mock: play_current_media called")
|
||||
|
||||
mock_app = MockApp()
|
||||
|
||||
# Test button to open touch-optimized settings
|
||||
def open_touch_settings():
|
||||
try:
|
||||
settings = SettingsWindow(root, mock_app)
|
||||
print("✅ Touch-optimized settings window opened successfully!")
|
||||
except Exception as e:
|
||||
print(f"❌ Error opening settings: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Large touch-friendly button
|
||||
settings_btn = tk.Button(welcome_frame,
|
||||
text="🔧 Open Touch Settings",
|
||||
command=open_touch_settings,
|
||||
bg='#3498db', fg='white',
|
||||
font=('Segoe UI', 16, 'bold'),
|
||||
relief=tk.FLAT, padx=40, pady=20,
|
||||
cursor='hand2')
|
||||
settings_btn.pack(pady=30)
|
||||
|
||||
# Instructions
|
||||
instructions = (
|
||||
"Touch Instructions:\n"
|
||||
"• Tap input fields to show virtual keyboard\n"
|
||||
"• Use large buttons for easy touch interaction\n"
|
||||
"• Virtual keyboard stays on top for easy access\n"
|
||||
"• Click outside input fields to hide keyboard"
|
||||
)
|
||||
|
||||
instr_label = tk.Label(welcome_frame, text=instructions,
|
||||
font=('Segoe UI', 11),
|
||||
fg='#bdc3c7', bg='#2c3e50',
|
||||
justify=tk.LEFT)
|
||||
instr_label.pack(pady=20)
|
||||
|
||||
# Exit button
|
||||
exit_btn = tk.Button(welcome_frame,
|
||||
text="❌ Exit Test",
|
||||
command=root.quit,
|
||||
bg='#e74c3c', fg='white',
|
||||
font=('Segoe UI', 12, 'bold'),
|
||||
relief=tk.FLAT, padx=30, pady=15,
|
||||
cursor='hand2')
|
||||
exit_btn.pack(pady=20)
|
||||
|
||||
# Add touch feedback to buttons
|
||||
def add_touch_feedback(button):
|
||||
def on_press(e):
|
||||
button.configure(relief=tk.SUNKEN)
|
||||
def on_release(e):
|
||||
button.configure(relief=tk.FLAT)
|
||||
def on_enter(e):
|
||||
button.configure(relief=tk.RAISED)
|
||||
def on_leave(e):
|
||||
button.configure(relief=tk.FLAT)
|
||||
|
||||
button.bind("<Button-1>", on_press)
|
||||
button.bind("<ButtonRelease-1>", on_release)
|
||||
button.bind("<Enter>", on_enter)
|
||||
button.bind("<Leave>", on_leave)
|
||||
|
||||
add_touch_feedback(settings_btn)
|
||||
add_touch_feedback(exit_btn)
|
||||
|
||||
print("🎮 Touch Display Test Started")
|
||||
print("=" * 50)
|
||||
print("Features being tested:")
|
||||
print("- Virtual keyboard integration")
|
||||
print("- Touch-optimized input fields")
|
||||
print("- Large, finger-friendly buttons")
|
||||
print("- Enhanced visual feedback")
|
||||
print("- Dark theme for displays")
|
||||
print("\nClick 'Open Touch Settings' to test the interface!")
|
||||
|
||||
root.mainloop()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error in touch interface test: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_virtual_keyboard_standalone():
|
||||
"""Test just the virtual keyboard component"""
|
||||
try:
|
||||
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
|
||||
|
||||
root = tk.Tk()
|
||||
root.title("🎹 Virtual Keyboard Test")
|
||||
root.geometry("800x500")
|
||||
root.configure(bg='#2f3136')
|
||||
|
||||
# Create virtual keyboard
|
||||
vk = VirtualKeyboard(root, dark_theme=True)
|
||||
|
||||
# Test interface
|
||||
test_frame = tk.Frame(root, bg='#2f3136', padx=30, pady=30)
|
||||
test_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
tk.Label(test_frame, text="🎹 Virtual Keyboard Test",
|
||||
font=('Segoe UI', 20, 'bold'),
|
||||
bg='#2f3136', fg='white').pack(pady=20)
|
||||
|
||||
tk.Label(test_frame, text="Click on the input fields below to test the virtual keyboard:",
|
||||
font=('Segoe UI', 12),
|
||||
bg='#2f3136', fg='#b9bbbe').pack(pady=10)
|
||||
|
||||
# Test input fields
|
||||
tk.Label(test_frame, text="Server IP:", bg='#2f3136', fg='white',
|
||||
font=('Segoe UI', 11, 'bold')).pack(anchor=tk.W, pady=(20, 5))
|
||||
entry1 = TouchOptimizedEntry(test_frame, vk, width=40, bg='#36393f',
|
||||
fg='white', insertbackground='white')
|
||||
entry1.pack(pady=5, fill=tk.X)
|
||||
|
||||
tk.Label(test_frame, text="Device Name:", bg='#2f3136', fg='white',
|
||||
font=('Segoe UI', 11, 'bold')).pack(anchor=tk.W, pady=(15, 5))
|
||||
entry2 = TouchOptimizedEntry(test_frame, vk, width=40, bg='#36393f',
|
||||
fg='white', insertbackground='white')
|
||||
entry2.pack(pady=5, fill=tk.X)
|
||||
|
||||
tk.Label(test_frame, text="Password:", bg='#2f3136', fg='white',
|
||||
font=('Segoe UI', 11, 'bold')).pack(anchor=tk.W, pady=(15, 5))
|
||||
entry3 = TouchOptimizedEntry(test_frame, vk, width=40, bg='#36393f',
|
||||
fg='white', insertbackground='white', show='*')
|
||||
entry3.pack(pady=5, fill=tk.X)
|
||||
|
||||
# Control buttons
|
||||
btn_frame = tk.Frame(test_frame, bg='#2f3136')
|
||||
btn_frame.pack(pady=30)
|
||||
|
||||
TouchOptimizedButton(btn_frame, text="🎹 Show Keyboard",
|
||||
command=lambda: vk.show_keyboard(entry1),
|
||||
bg='#7289da', fg='white').pack(side=tk.LEFT, padx=10)
|
||||
|
||||
TouchOptimizedButton(btn_frame, text="❌ Hide Keyboard",
|
||||
command=vk.hide_keyboard,
|
||||
bg='#ed4245', fg='white').pack(side=tk.LEFT, padx=10)
|
||||
|
||||
TouchOptimizedButton(btn_frame, text="🔄 Clear All",
|
||||
command=lambda: [e.delete(0, tk.END) for e in [entry1, entry2, entry3]],
|
||||
bg='#faa61a', fg='white').pack(side=tk.LEFT, padx=10)
|
||||
|
||||
print("🎹 Virtual Keyboard Test Started")
|
||||
print("- Click input fields to auto-show keyboard")
|
||||
print("- Type using virtual or physical keyboard")
|
||||
print("- Test touch-friendly interface")
|
||||
|
||||
root.mainloop()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error in virtual keyboard test: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Touch Display Tests")
|
||||
parser.add_argument("--keyboard-only", action="store_true",
|
||||
help="Test only the virtual keyboard component")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("🎮 Touch Display Digital Signage Tests")
|
||||
print("=" * 50)
|
||||
|
||||
if args.keyboard_only:
|
||||
test_virtual_keyboard_standalone()
|
||||
else:
|
||||
test_touch_interface()
|
||||
|
||||
print("\n✅ Touch display tests completed!")
|
||||
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 537 KiB |
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "demo1.jpg",
|
||||
"url": "Resurse/demo1.jpg",
|
||||
"duration": 20
|
||||
},
|
||||
{
|
||||
"file_name": "demo2.jpg",
|
||||
"url": "Resurse/demo2.jpg",
|
||||
"duration": 20
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg",
|
||||
"url": "static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg",
|
||||
"duration": 20
|
||||
},
|
||||
{
|
||||
"file_name": "wp2782770-1846651530.jpg",
|
||||
"url": "static/resurse/wp2782770-1846651530.jpg",
|
||||
"duration": 15
|
||||
},
|
||||
{
|
||||
"file_name": "SampleVideo_1280x720_1mb.mp4",
|
||||
"url": "static/resurse/SampleVideo_1280x720_1mb.mp4",
|
||||
"duration": 5
|
||||
}
|
||||
],
|
||||
"version": 0
|
||||
}
|
||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,27 +0,0 @@
|
||||
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)
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Main entry point for the tkinter-based signage player application.
|
||||
This file acts as the main executable for launching the tkinter player.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the current directory to the path so we can import our modules
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Import the player module from player_app.py
|
||||
from player_app import SimpleMediaPlayerApp
|
||||
|
||||
if __name__ == "__main__":
|
||||
import tkinter as tk
|
||||
root = tk.Tk()
|
||||
player = SimpleMediaPlayerApp(root)
|
||||
player.run()
|
||||
@@ -1,722 +0,0 @@
|
||||
# player_app.py
|
||||
# Main player application logic moved from tkinter_simple_player.py
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, simpledialog
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import requests # Required for server communication
|
||||
import queue
|
||||
import vlc # For video playback with hardware acceleration
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
PIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
PIL_AVAILABLE = False
|
||||
print("WARNING: PIL not available. Image display functionality will be limited.")
|
||||
|
||||
from python_functions import (
|
||||
load_local_playlist, download_media_files, clean_unused_files,
|
||||
save_local_playlist, update_config_playlist_version, fetch_server_playlist,
|
||||
load_config
|
||||
)
|
||||
from logging_config import Logger
|
||||
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
|
||||
from settings_screen import SettingsWindow
|
||||
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
|
||||
|
||||
|
||||
class SimpleMediaPlayerApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.running = True
|
||||
self.is_paused = False
|
||||
self.is_fullscreen = True
|
||||
self.playlist = []
|
||||
self.current_index = 0
|
||||
self.scaling_mode = 'fit'
|
||||
self.auto_advance_timer = None
|
||||
self.hide_controls_timer = None
|
||||
self.control_frame = None
|
||||
self.settings_window = None
|
||||
self.content_frame = None
|
||||
self.image_label = None
|
||||
self.status_label = None
|
||||
self.play_pause_btn = None
|
||||
self.prev_btn = None
|
||||
self.next_btn = None
|
||||
self.exit_btn = None
|
||||
self.settings_btn = None
|
||||
self.setup_window()
|
||||
self.setup_ui()
|
||||
# Automatically check for new server playlist on startup
|
||||
try:
|
||||
from python_functions import check_for_new_server_playlist
|
||||
check_for_new_server_playlist()
|
||||
except Exception as e:
|
||||
Logger.error(f"Failed to check for new server playlist: {e}")
|
||||
self.initialize_playlist_from_server()
|
||||
self.start_periodic_checks()
|
||||
|
||||
|
||||
def setup_window(self):
|
||||
self.root.title("Simple Signage Player")
|
||||
self.root.configure(bg='black')
|
||||
try:
|
||||
config = load_config()
|
||||
width = int(config.get('screen_w', 1920))
|
||||
height = int(config.get('screen_h', 1080))
|
||||
self.scaling_mode = config.get('scaling_mode', 'fit')
|
||||
except:
|
||||
width, height = 800, 600
|
||||
self.scaling_mode = 'fit'
|
||||
self.root.geometry(f"{width}x{height}")
|
||||
self.root.attributes('-fullscreen', True)
|
||||
self.root.bind('<Key>', self.on_key_press)
|
||||
self.root.bind('<Button-1>', self.on_mouse_click)
|
||||
self.root.bind('<Motion>', self.on_mouse_motion)
|
||||
self.root.focus_set()
|
||||
|
||||
def setup_ui(self):
|
||||
self.content_frame = tk.Frame(self.root, bg='black')
|
||||
self.content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
self.image_label = tk.Label(self.content_frame, bg='black')
|
||||
self.image_label.pack(fill=tk.BOTH, expand=True)
|
||||
self.status_label = tk.Label(
|
||||
self.content_frame,
|
||||
bg='black',
|
||||
fg='white',
|
||||
font=('Arial', 24),
|
||||
text=""
|
||||
)
|
||||
self.create_control_panel()
|
||||
self.show_controls()
|
||||
self.schedule_hide_controls()
|
||||
|
||||
# --- FULL METHOD BODIES FROM tkinter_simple_player_old.py BELOW ---
|
||||
def create_control_panel(self):
|
||||
"""Create touch-optimized control panel with larger buttons"""
|
||||
self.control_frame = tk.Frame(
|
||||
self.root,
|
||||
bg='#1a1a1a',
|
||||
bd=2,
|
||||
relief=tk.RAISED,
|
||||
padx=15,
|
||||
pady=15
|
||||
)
|
||||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||
button_config = {
|
||||
'bg': '#333333',
|
||||
'fg': 'white',
|
||||
'activebackground': '#555555',
|
||||
'activeforeground': 'white',
|
||||
'relief': tk.FLAT,
|
||||
'borderwidth': 0,
|
||||
'width': 10,
|
||||
'height': 3,
|
||||
'font': ('Segoe UI', 10, 'bold'),
|
||||
'cursor': 'hand2'
|
||||
}
|
||||
self.prev_btn = tk.Button(
|
||||
self.control_frame,
|
||||
text="⏮ Prev",
|
||||
command=self.previous_media,
|
||||
**button_config
|
||||
)
|
||||
self.prev_btn.grid(row=0, column=0, padx=5)
|
||||
self.play_pause_btn = tk.Button(
|
||||
self.control_frame,
|
||||
text="⏸ Pause" if not self.is_paused else "▶ Play",
|
||||
command=self.toggle_play_pause,
|
||||
bg='#27ae60',
|
||||
activebackground='#35d974',
|
||||
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
||||
)
|
||||
self.play_pause_btn.grid(row=0, column=1, padx=5)
|
||||
self.next_btn = tk.Button(
|
||||
self.control_frame,
|
||||
text="Next ⏭",
|
||||
command=self.next_media,
|
||||
**button_config
|
||||
)
|
||||
self.next_btn.grid(row=0, column=2, padx=5)
|
||||
self.settings_btn = tk.Button(
|
||||
self.control_frame,
|
||||
text="⚙️ Settings",
|
||||
command=self.open_settings,
|
||||
bg='#9b59b6',
|
||||
activebackground='#bb8fce',
|
||||
**{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']}
|
||||
)
|
||||
self.settings_btn.grid(row=0, column=3, padx=5)
|
||||
self.exit_btn = tk.Button(
|
||||
self.control_frame,
|
||||
text="❌ EXIT",
|
||||
command=self.show_exit_dialog,
|
||||
bg='#e74c3c',
|
||||
fg='white',
|
||||
activebackground='#ec7063',
|
||||
activeforeground='white',
|
||||
relief=tk.FLAT,
|
||||
borderwidth=0,
|
||||
width=8,
|
||||
height=3,
|
||||
font=('Segoe UI', 10, 'bold'),
|
||||
cursor='hand2'
|
||||
)
|
||||
self.exit_btn.grid(row=0, column=4, padx=5)
|
||||
for button in [self.prev_btn, self.play_pause_btn, self.next_btn, self.settings_btn, self.exit_btn]:
|
||||
self.add_touch_feedback_to_control_button(button)
|
||||
|
||||
def scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'):
|
||||
img_width, img_height = img.size
|
||||
if mode == 'stretch':
|
||||
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
|
||||
elif mode == 'fill':
|
||||
screen_ratio = screen_width / screen_height
|
||||
img_ratio = img_width / img_height
|
||||
if img_ratio > screen_ratio:
|
||||
new_height = screen_height
|
||||
new_width = int(screen_height * img_ratio)
|
||||
x_offset = (screen_width - new_width) // 2
|
||||
y_offset = 0
|
||||
else:
|
||||
new_width = screen_width
|
||||
new_height = int(screen_width / img_ratio)
|
||||
x_offset = 0
|
||||
y_offset = (screen_height - new_height) // 2
|
||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||
if new_width > screen_width:
|
||||
crop_x = (new_width - screen_width) // 2
|
||||
img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height))
|
||||
x_offset = 0
|
||||
if new_height > screen_height:
|
||||
crop_y = (new_height - screen_height) // 2
|
||||
img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height))
|
||||
y_offset = 0
|
||||
final_img.paste(img_resized, (x_offset, y_offset))
|
||||
return final_img, (x_offset, y_offset)
|
||||
else:
|
||||
screen_ratio = screen_width / screen_height
|
||||
img_ratio = img_width / img_height
|
||||
if img_ratio > screen_ratio:
|
||||
new_width = screen_width
|
||||
new_height = int(screen_width / img_ratio)
|
||||
else:
|
||||
new_height = screen_height
|
||||
new_width = int(screen_height * img_ratio)
|
||||
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
|
||||
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
|
||||
x_offset = (screen_width - new_width) // 2
|
||||
y_offset = (screen_height - new_height) // 2
|
||||
final_img.paste(img_resized, (x_offset, y_offset))
|
||||
return final_img, (x_offset, y_offset)
|
||||
|
||||
def add_touch_feedback_to_control_button(self, button):
|
||||
original_bg = button.cget('bg')
|
||||
def on_press(e):
|
||||
button.configure(relief=tk.SUNKEN)
|
||||
def on_release(e):
|
||||
button.configure(relief=tk.FLAT)
|
||||
def on_enter(e):
|
||||
button.configure(relief=tk.RAISED)
|
||||
def on_leave(e):
|
||||
button.configure(relief=tk.FLAT)
|
||||
button.bind("<Button-1>", on_press)
|
||||
button.bind("<ButtonRelease-1>", on_release)
|
||||
button.bind("<Enter>", on_enter)
|
||||
button.bind("<Leave>", on_leave)
|
||||
|
||||
def initialize_playlist_from_server(self):
|
||||
fallback_playlist = None
|
||||
try:
|
||||
local_playlist_data = load_local_playlist()
|
||||
fallback_playlist = local_playlist_data.get('playlist', [])
|
||||
if fallback_playlist:
|
||||
Logger.info(f"Found fallback playlist with {len(fallback_playlist)} items")
|
||||
except Exception as e:
|
||||
Logger.warning(f"No fallback playlist available: {e}")
|
||||
self.status_label.config(text="Connecting to server...\nPlease wait")
|
||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||
self.root.update()
|
||||
config = load_config()
|
||||
server = config.get("server_ip", "")
|
||||
host = config.get("screen_name", "")
|
||||
quick = config.get("quickconnect_key", "")
|
||||
port = config.get("port", "")
|
||||
Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}")
|
||||
if not server or not host or not quick or not port:
|
||||
Logger.warning("Missing server configuration, using fallback playlist")
|
||||
self.status_label.place_forget()
|
||||
self.load_fallback_playlist(fallback_playlist)
|
||||
return
|
||||
server_connection_successful = False
|
||||
try:
|
||||
Logger.info("Attempting to connect to server...")
|
||||
self.status_label.config(text="Connecting to server...\nAttempting connection")
|
||||
self.root.update()
|
||||
server_playlist_data = fetch_server_playlist()
|
||||
server_playlist = server_playlist_data.get('playlist', [])
|
||||
server_version = server_playlist_data.get('version', 0)
|
||||
if server_playlist:
|
||||
Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}")
|
||||
server_connection_successful = True
|
||||
self.status_label.config(text="Downloading media files...\nPlease wait")
|
||||
self.root.update()
|
||||
download_media_files(server_playlist, server_version)
|
||||
update_config_playlist_version(server_version)
|
||||
local_playlist_data = load_local_playlist()
|
||||
self.playlist = local_playlist_data.get('playlist', [])
|
||||
if self.playlist:
|
||||
Logger.info(f"Successfully loaded {len(self.playlist)} items from server")
|
||||
self.status_label.place_forget()
|
||||
self.play_current_media()
|
||||
return
|
||||
else:
|
||||
Logger.warning("Server playlist was empty, falling back to local playlist")
|
||||
else:
|
||||
Logger.warning("Server returned empty playlist, falling back to local playlist")
|
||||
except requests.exceptions.ConnectTimeout:
|
||||
Logger.error("Server connection timeout, using fallback playlist")
|
||||
except requests.exceptions.ConnectionError:
|
||||
Logger.error("Cannot connect to server, using fallback playlist")
|
||||
except requests.exceptions.Timeout:
|
||||
Logger.error("Server request timeout, using fallback playlist")
|
||||
except Exception as e:
|
||||
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
|
||||
if not server_connection_successful:
|
||||
self.status_label.config(text="Server unavailable\nLoading last playlist...")
|
||||
self.root.update()
|
||||
time.sleep(1)
|
||||
self.status_label.place_forget()
|
||||
self.load_fallback_playlist(fallback_playlist)
|
||||
|
||||
def load_fallback_playlist(self, fallback_playlist):
|
||||
if fallback_playlist and len(fallback_playlist) > 0:
|
||||
self.playlist = fallback_playlist
|
||||
Logger.info(f"Loaded fallback playlist with {len(self.playlist)} items")
|
||||
self.play_current_media()
|
||||
else:
|
||||
Logger.warning("No fallback playlist available, loading demo content")
|
||||
self.load_demo_or_local_playlist()
|
||||
|
||||
def load_demo_or_local_playlist(self):
|
||||
local_playlist_data = load_local_playlist()
|
||||
self.playlist = local_playlist_data.get('playlist', [])
|
||||
if self.playlist:
|
||||
Logger.info(f"Loaded existing local playlist with {len(self.playlist)} items")
|
||||
self.play_current_media()
|
||||
return
|
||||
Logger.info("No local playlist found, loading demo content")
|
||||
self.create_demo_content()
|
||||
if self.playlist:
|
||||
self.play_current_media()
|
||||
else:
|
||||
self.show_no_content_message()
|
||||
|
||||
def create_demo_content(self):
|
||||
demo_images = []
|
||||
static_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
|
||||
if os.path.exists(static_dir):
|
||||
for file in os.listdir(static_dir):
|
||||
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
||||
full_path = os.path.join(static_dir, file)
|
||||
demo_images.append({
|
||||
'file_name': file,
|
||||
'url': full_path,
|
||||
'duration': 5
|
||||
})
|
||||
if not demo_images:
|
||||
demo_dir = './Resurse'
|
||||
if os.path.exists(demo_dir):
|
||||
for file in os.listdir(demo_dir):
|
||||
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
||||
demo_images.append({
|
||||
'file_name': file,
|
||||
'url': os.path.join(demo_dir, file),
|
||||
'duration': 5
|
||||
})
|
||||
if demo_images:
|
||||
self.playlist = demo_images
|
||||
Logger.info(f"Created demo playlist with {len(demo_images)} images")
|
||||
else:
|
||||
self.playlist = [{
|
||||
'file_name': 'Demo Text',
|
||||
'url': 'text://Welcome to Tkinter Media Player!\n\nPlease configure server settings',
|
||||
'duration': 5
|
||||
}]
|
||||
|
||||
def show_no_content_message(self):
|
||||
self.image_label.config(image='')
|
||||
self.status_label.config(
|
||||
text="No media content available.\nPress Settings to configure server connection."
|
||||
)
|
||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||
|
||||
def show_error_message(self, message):
|
||||
self.image_label.config(image='')
|
||||
self.status_label.config(text=f"Error: {message}")
|
||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||
|
||||
def play_current_media(self):
|
||||
if not self.playlist or self.current_index >= len(self.playlist):
|
||||
self.show_no_content_message()
|
||||
return
|
||||
media = self.playlist[self.current_index]
|
||||
file_path = media.get('url', '')
|
||||
file_name = media.get('file_name', '')
|
||||
duration = media.get('duration', 10)
|
||||
if file_path.startswith('static/resurse/'):
|
||||
absolute_path = os.path.join(os.path.dirname(__file__), file_path)
|
||||
file_path = absolute_path
|
||||
Logger.info(f"Playing media: {file_name} from {file_path}")
|
||||
self.log_event(file_name, "STARTED")
|
||||
self.cancel_timers()
|
||||
if file_path.startswith('text://'):
|
||||
self.show_text_content(file_path[7:], duration)
|
||||
elif file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):
|
||||
self.play_video(file_path)
|
||||
elif os.path.exists(file_path) and file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp')):
|
||||
self.show_image(file_path, duration)
|
||||
else:
|
||||
Logger.error(f"Unsupported or missing media: {file_path}")
|
||||
self.status_label.config(text=f"Missing or unsupported media:\n{file_name}")
|
||||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||||
|
||||
def play_video(self, file_path):
|
||||
self.status_label.place_forget()
|
||||
def run_vlc_subprocess():
|
||||
try:
|
||||
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
|
||||
vlc_cmd = [
|
||||
'cvlc',
|
||||
'--fullscreen',
|
||||
'--no-osd',
|
||||
'--no-video-title-show',
|
||||
'--play-and-exit',
|
||||
'--quiet',
|
||||
file_path
|
||||
]
|
||||
proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
proc.wait()
|
||||
Logger.info(f"VLC subprocess finished: {file_path}")
|
||||
except Exception as e:
|
||||
Logger.error(f"VLC subprocess error: {e}")
|
||||
finally:
|
||||
self.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.root.after(1000, self.next_media)))
|
||||
threading.Thread(target=run_vlc_subprocess, daemon=True).start()
|
||||
|
||||
def _update_video_frame(self, photo):
|
||||
try:
|
||||
self.image_label.config(image=photo)
|
||||
self.image_label.image = photo
|
||||
except Exception as e:
|
||||
Logger.error(f"Error updating video frame: {e}")
|
||||
|
||||
def _show_video_error(self, error_msg):
|
||||
try:
|
||||
self.status_label.config(text=f"Video Error:\n{error_msg}")
|
||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||||
except Exception as e:
|
||||
Logger.error(f"Error showing video error: {e}")
|
||||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||||
|
||||
def show_text_content(self, text, duration):
|
||||
self.image_label.config(image='')
|
||||
self.status_label.config(text=text)
|
||||
self.auto_advance_timer = self.root.after(
|
||||
int(duration * 1000),
|
||||
self.next_media
|
||||
)
|
||||
|
||||
def show_image(self, file_path, duration):
|
||||
try:
|
||||
self.status_label.place_forget()
|
||||
self.status_label.config(text="")
|
||||
if PIL_AVAILABLE:
|
||||
img = Image.open(file_path)
|
||||
original_size = img.size
|
||||
screen_width = self.root.winfo_width()
|
||||
screen_height = self.root.winfo_height()
|
||||
if screen_width <= 1 or screen_height <= 1:
|
||||
screen_width = 1920
|
||||
screen_height = 1080
|
||||
final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
|
||||
photo = ImageTk.PhotoImage(final_img)
|
||||
self.image_label.config(image=photo)
|
||||
self.image_label.image = photo
|
||||
Logger.info(f"Successfully displayed image: {os.path.basename(file_path)} "
|
||||
f"(Original: {original_size}, Screen: {screen_width}x{screen_height}, "
|
||||
f"Mode: {self.scaling_mode}, Offset: {offset})")
|
||||
else:
|
||||
self.image_label.config(image='')
|
||||
self.status_label.config(text=f"IMAGE: {os.path.basename(file_path)}\n\n(Install PIL for image display)")
|
||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||
Logger.warning("PIL not available - showing text placeholder for image")
|
||||
self.auto_advance_timer = self.root.after(
|
||||
int(duration * 1000),
|
||||
self.next_media
|
||||
)
|
||||
except Exception as e:
|
||||
Logger.error(f"Failed to show image {file_path}: {e}")
|
||||
self.image_label.config(image='')
|
||||
self.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}")
|
||||
self.status_label.place(relx=0.5, rely=0.5, anchor='center')
|
||||
self.auto_advance_timer = self.root.after(5000, self.next_media)
|
||||
|
||||
def next_media(self):
|
||||
self.cancel_timers()
|
||||
if not self.playlist:
|
||||
return
|
||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||
if self.current_index == 0:
|
||||
threading.Thread(target=self.check_playlist_updates, daemon=True).start()
|
||||
self.play_current_media()
|
||||
|
||||
def previous_media(self):
|
||||
self.cancel_timers()
|
||||
if not self.playlist:
|
||||
return
|
||||
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||
self.play_current_media()
|
||||
|
||||
def toggle_play_pause(self):
|
||||
self.is_paused = not self.is_paused
|
||||
if self.is_paused:
|
||||
self.play_pause_btn.config(text="▶ Play")
|
||||
self.cancel_timers()
|
||||
else:
|
||||
self.play_pause_btn.config(text="⏸ Pause")
|
||||
self.play_current_media()
|
||||
Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}")
|
||||
|
||||
def cancel_timers(self):
|
||||
if self.auto_advance_timer:
|
||||
self.root.after_cancel(self.auto_advance_timer)
|
||||
self.auto_advance_timer = None
|
||||
|
||||
def show_controls(self):
|
||||
if self.control_frame:
|
||||
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
|
||||
|
||||
def hide_controls(self):
|
||||
if self.control_frame:
|
||||
self.control_frame.place_forget()
|
||||
|
||||
def schedule_hide_controls(self):
|
||||
if self.hide_controls_timer:
|
||||
self.root.after_cancel(self.hide_controls_timer)
|
||||
self.hide_controls_timer = self.root.after(10000, self.hide_controls)
|
||||
|
||||
def on_mouse_click(self, event):
|
||||
self.show_controls()
|
||||
self.schedule_hide_controls()
|
||||
|
||||
def on_mouse_motion(self, event):
|
||||
self.show_controls()
|
||||
self.schedule_hide_controls()
|
||||
|
||||
def on_key_press(self, event):
|
||||
key = event.keysym.lower()
|
||||
if key == 'f':
|
||||
self.toggle_fullscreen()
|
||||
elif key == 'space':
|
||||
self.toggle_play_pause()
|
||||
elif key == 'left':
|
||||
self.previous_media()
|
||||
elif key == 'right':
|
||||
self.next_media()
|
||||
elif key == 'escape':
|
||||
self.show_exit_dialog()
|
||||
elif key == '1':
|
||||
self.set_scaling_mode('fit')
|
||||
elif key == '2':
|
||||
self.set_scaling_mode('fill')
|
||||
elif key == '3':
|
||||
self.set_scaling_mode('stretch')
|
||||
elif event.state & 0x4:
|
||||
if key == 's':
|
||||
self.open_settings()
|
||||
self.show_controls()
|
||||
self.schedule_hide_controls()
|
||||
|
||||
def set_scaling_mode(self, mode):
|
||||
old_mode = self.scaling_mode
|
||||
self.scaling_mode = mode
|
||||
Logger.info(f"Scaling mode changed from '{old_mode}' to '{mode}'")
|
||||
self.status_label.config(text=f"Scaling Mode: {mode.title()}\n"
|
||||
f"1=Fit 2=Fill 3=Stretch")
|
||||
self.status_label.place(relx=0.5, rely=0.05, anchor='center')
|
||||
self.root.after(2000, lambda: self.status_label.place_forget())
|
||||
if self.playlist and 0 <= self.current_index < len(self.playlist):
|
||||
self.cancel_timers()
|
||||
self.play_current_media()
|
||||
|
||||
def toggle_fullscreen(self):
|
||||
self.is_fullscreen = not self.is_fullscreen
|
||||
self.root.attributes('-fullscreen', self.is_fullscreen)
|
||||
|
||||
def open_settings(self):
|
||||
if hasattr(self, 'settings_window') and self.settings_window and self.settings_window.winfo_exists():
|
||||
self.settings_window.lift()
|
||||
return
|
||||
if not self.is_paused:
|
||||
self.toggle_play_pause()
|
||||
self.settings_window = SettingsWindow(self.root, self)
|
||||
def on_settings_close():
|
||||
if self.is_paused:
|
||||
self.toggle_play_pause()
|
||||
self.settings_window.protocol("WM_DELETE_WINDOW", on_settings_close)
|
||||
|
||||
def show_exit_dialog(self):
|
||||
try:
|
||||
config = load_config()
|
||||
quickconnect_key = config.get('quickconnect_key', '')
|
||||
except:
|
||||
quickconnect_key = ''
|
||||
exit_dialog = tk.Toplevel(self.root)
|
||||
exit_dialog.title("Exit Application")
|
||||
exit_dialog.geometry("400x200")
|
||||
exit_dialog.configure(bg='#2d2d2d')
|
||||
exit_dialog.transient(self.root)
|
||||
exit_dialog.grab_set()
|
||||
exit_dialog.resizable(False, False)
|
||||
self.center_dialog_on_screen(exit_dialog, 400, 200)
|
||||
header_frame = tk.Frame(exit_dialog, bg='#cc0000', height=60)
|
||||
header_frame.pack(fill=tk.X)
|
||||
header_frame.pack_propagate(False)
|
||||
icon_label = tk.Label(header_frame, text="⚠", font=('Arial', 20, 'bold'),
|
||||
fg='white', bg='#cc0000')
|
||||
icon_label.pack(side=tk.LEFT, padx=15, pady=15)
|
||||
title_label = tk.Label(header_frame, text="Exit Application",
|
||||
font=('Arial', 14, 'bold'), fg='white', bg='#cc0000')
|
||||
title_label.pack(side=tk.LEFT, pady=15)
|
||||
content_frame = tk.Frame(exit_dialog, bg='#2d2d2d', padx=20, pady=20)
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
prompt_label = tk.Label(content_frame, text="Enter password to exit:",
|
||||
font=('Arial', 11), fg='white', bg='#2d2d2d')
|
||||
prompt_label.pack(pady=(0, 10))
|
||||
password_var = tk.StringVar()
|
||||
password_entry = tk.Entry(content_frame, textvariable=password_var,
|
||||
font=('Arial', 11), show='*', width=25,
|
||||
bg='#404040', fg='white', insertbackground='white',
|
||||
relief=tk.FLAT, bd=5)
|
||||
password_entry.pack(pady=(0, 15))
|
||||
password_entry.focus_set()
|
||||
button_frame = tk.Frame(content_frame, bg='#2d2d2d')
|
||||
button_frame.pack(fill=tk.X)
|
||||
def check_password():
|
||||
if password_var.get() == quickconnect_key:
|
||||
exit_dialog.destroy()
|
||||
self.exit_application()
|
||||
elif password_var.get():
|
||||
error_label.config(text="✗ Incorrect password", fg='#ff4444')
|
||||
password_entry.delete(0, tk.END)
|
||||
password_entry.focus_set()
|
||||
def cancel_exit():
|
||||
exit_dialog.destroy()
|
||||
error_label = tk.Label(content_frame, text="", font=('Arial', 9),
|
||||
bg='#2d2d2d')
|
||||
error_label.pack()
|
||||
cancel_btn = tk.Button(button_frame, text="Cancel", command=cancel_exit,
|
||||
bg='#555555', fg='white', font=('Arial', 10, 'bold'),
|
||||
relief=tk.FLAT, padx=20, pady=8, width=10)
|
||||
cancel_btn.pack(side=tk.RIGHT, padx=(10, 0))
|
||||
exit_btn = tk.Button(button_frame, text="Exit", command=check_password,
|
||||
bg='#cc0000', fg='white', font=('Arial', 10, 'bold'),
|
||||
relief=tk.FLAT, padx=20, pady=8, width=10)
|
||||
exit_btn.pack(side=tk.RIGHT)
|
||||
password_entry.bind('<Return>', lambda e: check_password())
|
||||
exit_dialog.bind('<Escape>', lambda e: cancel_exit())
|
||||
|
||||
def exit_application(self):
|
||||
Logger.info("Application exit requested")
|
||||
self.running = False
|
||||
self.root.quit()
|
||||
self.root.destroy()
|
||||
|
||||
def center_dialog_on_screen(self, dialog, width, height):
|
||||
dialog.update_idletasks()
|
||||
screen_width = dialog.winfo_screenwidth()
|
||||
screen_height = dialog.winfo_screenheight()
|
||||
center_x = int((screen_width - width) / 2)
|
||||
center_y = int((screen_height - height) / 2)
|
||||
center_x = max(0, min(center_x, screen_width - width))
|
||||
center_y = max(0, min(center_y, screen_height - height))
|
||||
dialog.geometry(f"{width}x{height}+{center_x}+{center_y}")
|
||||
dialog.lift()
|
||||
dialog.focus_force()
|
||||
return center_x, center_y
|
||||
|
||||
def check_playlist_updates(self):
|
||||
try:
|
||||
config = load_config()
|
||||
local_version = config.get('playlist_version', 0)
|
||||
server_playlist_data = fetch_server_playlist()
|
||||
server_version = server_playlist_data.get('version', 0)
|
||||
if server_version > local_version:
|
||||
Logger.info(f"Updating playlist: {local_version} -> {server_version}")
|
||||
local_playlist_data = load_local_playlist()
|
||||
clean_unused_files(local_playlist_data.get('playlist', []))
|
||||
download_media_files(
|
||||
server_playlist_data.get('playlist', []),
|
||||
server_version
|
||||
)
|
||||
local_playlist_data = load_local_playlist()
|
||||
self.playlist = local_playlist_data.get('playlist', [])
|
||||
self.current_index = 0
|
||||
Logger.info("Playlist updated successfully")
|
||||
self.play_current_media()
|
||||
else:
|
||||
Logger.info("No playlist updates available")
|
||||
except requests.exceptions.ConnectTimeout:
|
||||
Logger.warning("Server connection timeout during update check - continuing with current playlist")
|
||||
except requests.exceptions.ConnectionError:
|
||||
Logger.warning("Cannot connect to server during update check - continuing with current playlist")
|
||||
except requests.exceptions.Timeout:
|
||||
Logger.warning("Server request timeout during update check - continuing with current playlist")
|
||||
except Exception as e:
|
||||
Logger.warning(f"Failed to check playlist updates: {e} - continuing with current playlist")
|
||||
|
||||
def log_event(self, file_name, event):
|
||||
try:
|
||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
log_message = f"{timestamp} - {event}: {file_name}\n"
|
||||
log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt')
|
||||
with open(log_file, 'a') as f:
|
||||
f.write(log_message)
|
||||
except Exception as e:
|
||||
Logger.error(f"Failed to log event: {e}")
|
||||
|
||||
def start_periodic_checks(self):
|
||||
def check_loop():
|
||||
while self.running:
|
||||
try:
|
||||
time.sleep(300)
|
||||
if self.running:
|
||||
self.check_playlist_updates()
|
||||
except Exception as e:
|
||||
Logger.error(f"Error in periodic check: {e}")
|
||||
threading.Thread(target=check_loop, daemon=True).start()
|
||||
|
||||
def run(self):
|
||||
Logger.info("Starting Simple Tkinter Media Player")
|
||||
try:
|
||||
self.root.mainloop()
|
||||
except KeyboardInterrupt:
|
||||
self.exit_application()
|
||||
except Exception as e:
|
||||
Logger.error(f"Application error: {e}")
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
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}")
|
||||
|
||||
def check_for_new_server_playlist():
|
||||
"""Check if the server has a new playlist version compared to app_config.txt."""
|
||||
config = load_config()
|
||||
local_version = config.get('playlist_version', 0)
|
||||
server_data = fetch_server_playlist()
|
||||
server_version = server_data.get('version', 0)
|
||||
if server_version > local_version:
|
||||
print(f"A new playlist is available on the server: version {server_version} (local: {local_version})")
|
||||
Logger.info(f"A new playlist is available on the server: version {server_version} (local: {local_version})")
|
||||
return True
|
||||
else:
|
||||
print(f"No new playlist on the server. Local version: {local_version}, Server version: {server_version}")
|
||||
Logger.info(f"No new playlist on the server. Local version: {local_version}, Server version: {server_version}")
|
||||
return False
|
||||