Remove Windows-era leftovers and document the cleanup
Removes development scratch that belonged to the Windows port and has no purpose on the Linux/Pi branch. 39 tracked files removed; everything is recoverable from the Windows-Player branch. Removed ------- - working_files/ (29 files) - dev-era scratch: install.sh.bak, the superseded get_playlists.py (v1, replaced by get_playlists_v2.py), one-off test_*.py probes, MIGRATION_GUIDE.md, INVESTIGATION_RESULTS.md. Also held real server captures (server_response_debug.json contained player_id, player name and a playlist) which should not be in the repository at all. - documentation/ (5 files) - all described the Windows-era HTTPS integration. - "python version" - contained Python 3.12.9, the Windows build interpreter. The Pi runs 3.13.5. - test_edited_media_upload.py - parentless debug script, referenced by nothing. - .display-keepalive.sh, .keep-screen-alive.sh, .wait-for-display.sh - orphan X11 helpers referenced by nothing; superseded by linux/linux_display.py. Kept deliberately ----------------- - .video-optimization.sh, .run-background.sh, .start-player-cron.sh - referenced by install.sh. - repo/python-wheels/ (58 MB of aarch64 cp313 wheels) - the offline install set. - .venv/ is gitignored; runtime caches (.kiosk-profile, .kivy), logs, media and the playlist cache are regenerated automatically and stay ignored. Also fixed a stale reference to the removed documentation/ directory in linux/development-track.md, and recorded the above there. Verified after removal: all modules under src/ import cleanly from an unrelated working directory, all remaining shell scripts pass `bash -n`, and the running player was unaffected (correct: it had already loaded its code).
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Aggressive display keep-alive for Raspberry Pi
|
||||
# Supports both X11 and Wayland environments
|
||||
|
||||
DISPLAY_TIMEOUT=30
|
||||
|
||||
# Detect display server type
|
||||
detect_display_server() {
|
||||
if [ -n "$WAYLAND_DISPLAY" ]; then
|
||||
echo "wayland"
|
||||
elif [ -n "$DISPLAY" ]; then
|
||||
echo "x11"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
DISPLAY_SERVER=$(detect_display_server)
|
||||
|
||||
while true; do
|
||||
# Keep HDMI powered on (works for both X11 and Wayland)
|
||||
if command -v tvservice &> /dev/null; then
|
||||
/usr/bin/tvservice -p 2>/dev/null
|
||||
fi
|
||||
|
||||
if [ "$DISPLAY_SERVER" = "wayland" ]; then
|
||||
# Wayland-specific power management
|
||||
|
||||
# Method 1: Use wlr-randr for Wayland compositors (if available)
|
||||
if command -v wlr-randr &> /dev/null; then
|
||||
wlr-randr --output HDMI-A-1 --on 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Method 2: Prevent idle using systemd-inhibit
|
||||
if command -v systemd-inhibit &> /dev/null; then
|
||||
# This is already running, but refresh the lock
|
||||
systemctl --user restart plasma-ksmserver.service 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Method 3: Use wlopm (Wayland output power management)
|
||||
if command -v wlopm &> /dev/null; then
|
||||
wlopm --on \* 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Method 4: Simulate activity via input (works on Wayland)
|
||||
if command -v ydotool &> /dev/null; then
|
||||
ydotool mousemove -x 1 -y 1 2>/dev/null || true
|
||||
ydotool mousemove -x -1 -y -1 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Method 5: GNOME/KDE Wayland idle inhibit
|
||||
if command -v gnome-session-inhibit &> /dev/null; then
|
||||
# Already inhibited by running process
|
||||
true
|
||||
fi
|
||||
|
||||
else
|
||||
# X11-specific power management (original code)
|
||||
if command -v xset &> /dev/null; then
|
||||
DISPLAY=:0 xset s off 2>/dev/null
|
||||
DISPLAY=:0 xset -dpms 2>/dev/null
|
||||
DISPLAY=:0 xset dpms force on 2>/dev/null
|
||||
DISPLAY=:0 xset s reset 2>/dev/null
|
||||
fi
|
||||
|
||||
# Move mouse to trigger activity
|
||||
if command -v xdotool &> /dev/null; then
|
||||
DISPLAY=:0 xdotool mousemove_relative 1 1 2>/dev/null
|
||||
DISPLAY=:0 xdotool mousemove_relative -1 -1 2>/dev/null
|
||||
fi
|
||||
|
||||
# Disable monitor power saving
|
||||
if command -v xrandr &> /dev/null; then
|
||||
DISPLAY=:0 xrandr --output HDMI-1 --power-profile performance 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep $DISPLAY_TIMEOUT
|
||||
done
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Keep-screen-alive wrapper for player
|
||||
# Prevents screen from locking/turning off while player is running
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Function to keep screen awake
|
||||
keep_screen_awake() {
|
||||
while true; do
|
||||
# Move mouse slightly to prevent idle
|
||||
if command -v xdotool &> /dev/null; then
|
||||
xdotool mousemove_relative 1 1
|
||||
xdotool mousemove_relative -1 -1
|
||||
fi
|
||||
|
||||
# Disable DPMS and screensaver periodically
|
||||
if command -v xset &> /dev/null; then
|
||||
xset s reset
|
||||
xset dpms force on
|
||||
fi
|
||||
|
||||
sleep 30
|
||||
done
|
||||
}
|
||||
|
||||
# Function to inhibit systemd sleep (if available)
|
||||
inhibit_sleep() {
|
||||
if command -v systemd-inhibit &> /dev/null; then
|
||||
# Run player under systemd inhibit to prevent sleep
|
||||
systemd-inhibit --what=sleep --why="Signage player running" \
|
||||
bash "$SCRIPT_DIR/start.sh"
|
||||
return $?
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Try systemd inhibit first (most reliable)
|
||||
if inhibit_sleep; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fallback: Start keep-alive in background
|
||||
keep_screen_awake &
|
||||
KEEPALIVE_PID=$!
|
||||
|
||||
# Start the player
|
||||
cd "$SCRIPT_DIR"
|
||||
bash start.sh
|
||||
PLAYER_EXIT=$?
|
||||
|
||||
# Kill keep-alive when player exits
|
||||
kill $KEEPALIVE_PID 2>/dev/null || true
|
||||
|
||||
exit $PLAYER_EXIT
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Wait for display server to be ready before starting the app
|
||||
# This prevents Kivy from failing to initialize graphics
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MAX_WAIT=60
|
||||
ELAPSED=0
|
||||
|
||||
echo "[$(date)] Waiting for display server to be ready..."
|
||||
|
||||
# Wait for display socket/device to appear
|
||||
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||
# Check for Wayland socket (primary for Bookworm)
|
||||
if [ -S "$XDG_RUNTIME_DIR/wayland-0" ] 2>/dev/null; then
|
||||
echo "[$(date)] ✓ Wayland display socket found"
|
||||
export WAYLAND_DISPLAY=wayland-0
|
||||
break
|
||||
fi
|
||||
|
||||
# Check for X11 display
|
||||
if [ -S "$XDG_RUNTIME_DIR/X11/display:0" ] 2>/dev/null; then
|
||||
echo "[$(date)] ✓ X11 display socket found"
|
||||
export DISPLAY=:0
|
||||
break
|
||||
fi
|
||||
|
||||
# Check if display manager is running (for fallback)
|
||||
if pgrep -f "wayland|weston|gnome-shell|xfwm4|openbox" > /dev/null 2>&1; then
|
||||
echo "[$(date)] ✓ Display manager detected"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "[$(date)] Waiting for display... ($ELAPSED/$MAX_WAIT seconds)"
|
||||
sleep 1
|
||||
((ELAPSED++))
|
||||
done
|
||||
|
||||
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
||||
echo "[$(date)] ⚠️ Display timeout after $MAX_WAIT seconds, proceeding anyway..."
|
||||
fi
|
||||
|
||||
# Set default display if not detected
|
||||
if [ -z "$WAYLAND_DISPLAY" ] && [ -z "$DISPLAY" ]; then
|
||||
echo "[$(date)] Using fallback display settings"
|
||||
export DISPLAY=:0
|
||||
export WAYLAND_DISPLAY=wayland-0
|
||||
fi
|
||||
|
||||
echo "[$(date)] Environment: DISPLAY=$DISPLAY WAYLAND_DISPLAY=$WAYLAND_DISPLAY"
|
||||
echo "[$(date)] XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
|
||||
|
||||
# Now start the app
|
||||
cd "$SCRIPT_DIR" || exit 1
|
||||
exec bash start.sh
|
||||
@@ -1,274 +0,0 @@
|
||||
# HTTPS Implementation Checklist
|
||||
|
||||
## Pre-Deployment
|
||||
|
||||
### Server Requirements
|
||||
- [ ] Server has HTTPS enabled on port 443
|
||||
- [ ] Server has valid SSL certificate (or self-signed)
|
||||
- [ ] `/api/certificate` endpoint is implemented
|
||||
- [ ] CORS headers are configured
|
||||
- [ ] All API endpoints support HTTPS
|
||||
|
||||
### Configuration Preparation
|
||||
- [ ] `config/app_config.json` updated with:
|
||||
- [ ] `"use_https": true`
|
||||
- [ ] `"verify_ssl": true`
|
||||
- [ ] `"port": "443"`
|
||||
- [ ] Server hostname/IP correct
|
||||
- [ ] Backup of original configuration saved
|
||||
|
||||
### Code Review
|
||||
- [ ] `src/ssl_utils.py` reviewed
|
||||
- [ ] `src/player_auth.py` changes reviewed
|
||||
- [ ] `src/get_playlists_v2.py` changes reviewed
|
||||
- [ ] `src/main.py` changes reviewed
|
||||
- [ ] All syntax verified (python3 -m py_compile)
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Pre-Deployment Testing
|
||||
- [ ] All Python files compile without errors
|
||||
- [ ] JSON configuration is valid
|
||||
- [ ] No import errors when loading modules
|
||||
- [ ] Certificate storage directory can be created (`~/.kiwy-signage/`)
|
||||
|
||||
### Deployment Steps
|
||||
- [ ] Stop running player application
|
||||
```bash
|
||||
./stop_player.sh
|
||||
```
|
||||
- [ ] Copy updated files to deployment location
|
||||
- [ ] Verify configuration is in place
|
||||
- [ ] Start application
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### Initial Verification (First 5 minutes)
|
||||
- [ ] Application starts without errors
|
||||
- [ ] Check logs for startup messages
|
||||
- [ ] Verify no SSL connection errors immediately
|
||||
- [ ] Check that certificate wasn't attempted to download (if server is unreachable, this is expected)
|
||||
|
||||
---
|
||||
|
||||
## Post-Deployment Testing
|
||||
|
||||
### Connection Test
|
||||
- [ ] Open settings UI on player
|
||||
- [ ] Enter server details (if not pre-configured)
|
||||
- [ ] Click "Test Connection" button
|
||||
- [ ] Connection succeeds with green checkmark
|
||||
- [ ] Error message is clear if connection fails
|
||||
|
||||
### Playlist Operations
|
||||
- [ ] Playlist fetches successfully from HTTPS server
|
||||
- [ ] Media files download without SSL errors
|
||||
- [ ] Playlist updates trigger correctly
|
||||
- [ ] No "CERTIFICATE_VERIFY_FAILED" errors in logs
|
||||
|
||||
### Certificate Management
|
||||
- [ ] Certificate file created: `~/.kiwy-signage/server_cert.pem`
|
||||
- [ ] Certificate info file created: `~/.kiwy-signage/cert_info.json`
|
||||
- [ ] Certificate can be verified:
|
||||
```bash
|
||||
openssl x509 -in ~/.kiwy-signage/server_cert.pem -text -noout
|
||||
```
|
||||
|
||||
### API Operations
|
||||
- [ ] Authentication succeeds over HTTPS
|
||||
- [ ] Playlist retrieval works
|
||||
- [ ] Media downloads work
|
||||
- [ ] Status feedback sends successfully
|
||||
- [ ] Heartbeat messages send without errors
|
||||
|
||||
---
|
||||
|
||||
## Monitoring (24-48 hours)
|
||||
|
||||
### Log Review
|
||||
- [ ] Check application logs for SSL-related messages
|
||||
- [ ] Look for:
|
||||
- [ ] "Using saved certificate" or "Using system CA bundle"
|
||||
- [ ] "✓ Server certificate installed" (if auto-downloaded)
|
||||
- [ ] No SSL errors after certificate is loaded
|
||||
- [ ] All API operations succeeded
|
||||
|
||||
### Error Scenarios
|
||||
- [ ] If `SSL: CERTIFICATE_VERIFY_FAILED`:
|
||||
- [ ] Check server certificate is valid
|
||||
- [ ] Check `/api/certificate` endpoint returns proper certificate
|
||||
- [ ] Consider `verify_ssl: false` for testing (temporary only)
|
||||
|
||||
- [ ] If connection timeout:
|
||||
- [ ] Check network connectivity
|
||||
- [ ] Verify HTTPS port 443 is open
|
||||
- [ ] Check server is responding
|
||||
- [ ] Consider increasing timeout value
|
||||
|
||||
### Performance
|
||||
- [ ] HTTPS connections perform at acceptable speed
|
||||
- [ ] Media downloads at expected speed
|
||||
- [ ] No CPU spikes from SSL operations
|
||||
- [ ] Memory usage stable
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan (if needed)
|
||||
|
||||
If HTTPS deployment has issues:
|
||||
|
||||
1. **Quick Fallback to HTTP:**
|
||||
```json
|
||||
{
|
||||
"use_https": false,
|
||||
"port": "5000"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Steps:**
|
||||
- [ ] Update `app_config.json` with HTTP settings
|
||||
- [ ] Stop player: `./stop_player.sh`
|
||||
- [ ] Start player: `./start.sh`
|
||||
- [ ] Verify connection works
|
||||
|
||||
3. **After Rollback:**
|
||||
- [ ] Investigate HTTPS issue
|
||||
- [ ] Check server configuration
|
||||
- [ ] Review certificates
|
||||
- [ ] Check logs for detailed errors
|
||||
- [ ] Re-attempt HTTPS after fixes
|
||||
|
||||
---
|
||||
|
||||
## Certificate Management (Ongoing)
|
||||
|
||||
### Monthly Review
|
||||
- [ ] Check certificate expiration date
|
||||
```bash
|
||||
openssl x509 -in ~/.kiwy-signage/server_cert.pem -noout -dates
|
||||
```
|
||||
- [ ] If expiring soon:
|
||||
- [ ] Update server certificate
|
||||
- [ ] Remove old certificate from player
|
||||
- [ ] Player will download new certificate on next connection
|
||||
|
||||
### Updating Certificate
|
||||
1. Update server certificate
|
||||
2. Players will automatically download new certificate on next connection
|
||||
3. Or manually delete old certificate:
|
||||
```bash
|
||||
rm ~/.kiwy-signage/server_cert.pem
|
||||
```
|
||||
4. Next connection will download new certificate
|
||||
|
||||
### Monitoring Certificate Changes
|
||||
- [ ] Watch logs for "downloading server certificate"
|
||||
- [ ] Verify new certificate fingerprint in logs
|
||||
- [ ] Confirm all players successfully updated
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist (Comprehensive)
|
||||
|
||||
### Unit Tests
|
||||
- [ ] `ssl_utils.py` SSLManager class works
|
||||
- [ ] `player_auth.py` authentication with HTTPS
|
||||
- [ ] `get_playlists_v2.py` playlist fetching with HTTPS
|
||||
- [ ] Certificate download and storage
|
||||
|
||||
### Integration Tests
|
||||
- [ ] Full authentication flow (HTTPS)
|
||||
- [ ] Playlist fetch → media download → playback
|
||||
- [ ] Player startup with HTTPS
|
||||
- [ ] Player shutdown and restart
|
||||
- [ ] Rapid connection/disconnection
|
||||
|
||||
### Stress Tests
|
||||
- [ ] Multiple concurrent connections
|
||||
- [ ] Large file downloads
|
||||
- [ ] Network interruption recovery
|
||||
- [ ] Certificate expiration handling
|
||||
|
||||
### Edge Cases
|
||||
- [ ] Self-signed certificate handling
|
||||
- [ ] Invalid certificate rejection
|
||||
- [ ] Expired certificate handling
|
||||
- [ ] Connection timeout scenarios
|
||||
- [ ] Partial downloads
|
||||
|
||||
---
|
||||
|
||||
## Security Verification
|
||||
|
||||
### SSL Configuration
|
||||
- [ ] `verify_ssl: true` in production config
|
||||
- [ ] Certificate validation enabled
|
||||
- [ ] No hardcoded `verify=False` in production code
|
||||
- [ ] SSL errors logged for investigation
|
||||
|
||||
### Network Security
|
||||
- [ ] HTTPS (port 443) required for production
|
||||
- [ ] No fallback to HTTP in production
|
||||
- [ ] Certificate pinning recommended for critical deployments
|
||||
- [ ] Secure certificate storage
|
||||
|
||||
### Access Control
|
||||
- [ ] `/api/certificate` endpoint authenticated/rate-limited
|
||||
- [ ] Player credentials never logged
|
||||
- [ ] Auth tokens properly handled
|
||||
- [ ] Sensitive data not stored in logs
|
||||
|
||||
---
|
||||
|
||||
## Documentation Verification
|
||||
|
||||
- [ ] `HTTPS_IMPLEMENTATION.md` is accurate
|
||||
- [ ] `HTTPS_QUICK_REFERENCE.md` has working examples
|
||||
- [ ] `IMPLEMENTATION_COMPLETE.md` is up-to-date
|
||||
- [ ] Integration guide (`integration_guide.md`) matches implementation
|
||||
- [ ] Troubleshooting guide covers known issues
|
||||
|
||||
---
|
||||
|
||||
## Sign-Off
|
||||
|
||||
- [ ] Implementation complete and tested
|
||||
- [ ] All checklists items verified
|
||||
- [ ] Documentation reviewed
|
||||
- [ ] Ready for production deployment
|
||||
|
||||
**Date Completed:** ________________
|
||||
|
||||
**Tested By:** ________________________
|
||||
|
||||
**Approved By:** ________________________
|
||||
|
||||
---
|
||||
|
||||
## Notes & Issues Found
|
||||
|
||||
```
|
||||
[Space for documenting any issues encountered during deployment]
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Certificate pinning implementation
|
||||
- [ ] Automatic certificate renewal
|
||||
- [ ] Hardware security module support
|
||||
- [ ] Certificate chain validation
|
||||
- [ ] Monitoring/alerting for certificate issues
|
||||
- [ ] Certificate backup and restore
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** January 16, 2026
|
||||
**Status:** Ready for Production
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
# HTTPS Integration Implementation Summary
|
||||
|
||||
## Overview
|
||||
The Kiwy-Signage application has been successfully updated to support HTTPS requests to the server, implementing secure certificate management and SSL verification as outlined in the integration_guide.md.
|
||||
|
||||
---
|
||||
|
||||
## Files Created
|
||||
|
||||
### 1. **ssl_utils.py** (New Module)
|
||||
**Location:** `src/ssl_utils.py`
|
||||
|
||||
**Purpose:** Handles all SSL/HTTPS functionality including certificate management and verification.
|
||||
|
||||
**Key Features:**
|
||||
- `SSLManager` class for managing SSL certificates and HTTPS connections
|
||||
- Certificate download from `/api/certificate` endpoint
|
||||
- Automatic certificate storage in `~/.kiwy-signage/`
|
||||
- Configurable SSL verification (disabled for development, enabled for production)
|
||||
- Session management with proper SSL configuration
|
||||
- Helper function `setup_ssl_for_requests()` for quick SSL setup
|
||||
|
||||
**Key Methods:**
|
||||
- `download_server_certificate()` - Downloads and saves server certificate
|
||||
- `get_session()` - Returns SSL-configured requests session
|
||||
- `has_certificate()` - Checks if certificate is saved
|
||||
- `get_certificate_info()` - Retrieves saved certificate metadata
|
||||
- `validate_url_scheme()` - Ensures URLs use HTTPS
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
### 2. **player_auth.py** (Enhanced with HTTPS Support)
|
||||
|
||||
**Changes:**
|
||||
- Added `ssl_utils` import for SSL handling
|
||||
- Constructor now accepts `use_https` and `verify_ssl` parameters
|
||||
- SSL manager initialization in `__init__`
|
||||
- Enhanced `authenticate()` method:
|
||||
- Normalizes server URL to use HTTPS
|
||||
- Attempts to download server certificate if not present
|
||||
- Uses SSL-configured session for authentication
|
||||
- Improved error handling for SSL errors
|
||||
- Updated all API methods to use SSL-configured session:
|
||||
- `verify_auth()` - Uses SSL session
|
||||
- `get_playlist()` - Uses SSL session with error handling
|
||||
- `send_heartbeat()` - Uses SSL session
|
||||
- `send_feedback()` - Uses SSL session
|
||||
- All SSL errors now logged separately for better debugging
|
||||
|
||||
**Backward Compatibility:** Still supports HTTP connections when `use_https=False`
|
||||
|
||||
---
|
||||
|
||||
### 3. **get_playlists_v2.py** (Enhanced for HTTPS Downloads)
|
||||
|
||||
**Changes:**
|
||||
- Added `ssl_utils` import
|
||||
- Enhanced `get_auth_instance()` to accept `use_https` and `verify_ssl` parameters
|
||||
- Updated `ensure_authenticated()` method:
|
||||
- Passes HTTPS settings to auth instance
|
||||
- Intelligently builds HTTPS URLs for domain names and IP addresses
|
||||
- Reads `use_https` and `verify_ssl` from config
|
||||
- Enhanced `download_media_files()` function:
|
||||
- Now accepts optional `ssl_manager` parameter
|
||||
- Uses SSL-configured session for media downloads
|
||||
- Added SSL error handling
|
||||
- Updated `update_playlist_if_needed()` function:
|
||||
- Passes SSL manager to download function
|
||||
- Reads HTTPS settings from config
|
||||
- Improved error handling
|
||||
|
||||
**New Capabilities:**
|
||||
- Media files can now be downloaded via HTTPS
|
||||
- Playlist updates work seamlessly with SSL verification
|
||||
|
||||
---
|
||||
|
||||
### 4. **main.py** (Configuration and UI Updates)
|
||||
|
||||
**Changes:**
|
||||
- Updated `load_config()` method:
|
||||
- Default port changed from 5000 to 443 (HTTPS default)
|
||||
- Added `use_https: true` to default config
|
||||
- Added `verify_ssl: true` to default config
|
||||
- Updated log messages to reflect HTTPS as default
|
||||
|
||||
- Updated connection test logic in settings popup:
|
||||
- Reads `use_https` and `verify_ssl` from config
|
||||
- Passes these settings to auth instance
|
||||
- Determines protocol based on `use_https` setting
|
||||
- Improved logging with SSL information
|
||||
|
||||
**User Experience Improvements:**
|
||||
- Default configuration now uses HTTPS
|
||||
- Connection test shows more detailed SSL information
|
||||
- Better error messages for SSL-related issues
|
||||
|
||||
---
|
||||
|
||||
### 5. **app_config.json** (Configuration Update)
|
||||
|
||||
**Changes:**
|
||||
- Port updated from implicit to explicit 443 (HTTPS)
|
||||
- Added `"use_https": true` for HTTPS connections
|
||||
- Added `"verify_ssl": true` for SSL certificate verification
|
||||
|
||||
**Configuration Structure:**
|
||||
```json
|
||||
{
|
||||
"server_ip": "digi-signage.moto-adv.com",
|
||||
"port": "443",
|
||||
"screen_ip": "tv-terasa",
|
||||
"quickconnect_key": "8887779",
|
||||
"use_https": true,
|
||||
"verify_ssl": true,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### SSL Certificate Flow
|
||||
|
||||
1. **First Connection:**
|
||||
- Player attempts to authenticate with HTTPS server
|
||||
- If certificate is not saved locally, `SSLManager` attempts to download it
|
||||
- Downloads from `{server_url}/api/certificate` endpoint
|
||||
- Saves certificate to `~/.kiwy-signage/server_cert.pem`
|
||||
- All subsequent connections use saved certificate
|
||||
|
||||
2. **Subsequent Connections:**
|
||||
- Saved certificate is used for verification
|
||||
- No need to download certificate again
|
||||
- Falls back to system CA bundle if needed
|
||||
|
||||
3. **Certificate Storage:**
|
||||
- Location: `~/.kiwy-signage/`
|
||||
- Files:
|
||||
- `server_cert.pem` - Server certificate in PEM format
|
||||
- `cert_info.json` - Certificate metadata (issuer, validity dates, etc.)
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Setting | Type | Default | Purpose |
|
||||
|---------|------|---------|---------|
|
||||
| `use_https` | boolean | true | Enable/disable HTTPS |
|
||||
| `verify_ssl` | boolean | true | Enable/disable SSL verification |
|
||||
| `server_ip` | string | - | Server hostname or IP |
|
||||
| `port` | string | 443 | Server port |
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **SSL Certificate Errors:** Caught and logged separately
|
||||
- **Connection Errors:** Handled gracefully with fallback options
|
||||
- **Timeout Errors:** Configurable timeout with retry logic
|
||||
- **Development Mode:** Can disable SSL verification with `verify_ssl: false`
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Production Deployment
|
||||
|
||||
1. **Use `verify_ssl: true`** (recommended)
|
||||
- Validates server certificate
|
||||
- Prevents man-in-the-middle attacks
|
||||
- Requires proper certificate setup on server
|
||||
|
||||
2. **Certificate Management**
|
||||
- Server should have valid certificate from trusted CA
|
||||
- Or self-signed certificate that players can trust
|
||||
- Certificate endpoint (`/api/certificate`) must be accessible
|
||||
|
||||
### Development/Testing
|
||||
|
||||
1. **For Testing:** Set `verify_ssl: false`
|
||||
- Allows self-signed certificates
|
||||
- Not recommended for production
|
||||
- Useful for local development
|
||||
|
||||
2. **Certificate Distribution**
|
||||
- Use `/api/certificate` endpoint to distribute certificates
|
||||
- Certificates stored in secure location on device
|
||||
- Certificate update mechanism available
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Basic Connectivity
|
||||
- [ ] Player connects to HTTPS server
|
||||
- [ ] Certificate is downloaded automatically on first connection
|
||||
- [ ] Subsequent connections use saved certificate
|
||||
- [ ] Certificate info is displayed correctly
|
||||
|
||||
### Playlist Operations
|
||||
- [ ] Playlist fetches work with HTTPS
|
||||
- [ ] Media files download via HTTPS
|
||||
- [ ] Playlist updates without SSL errors
|
||||
- [ ] Status feedback sends successfully
|
||||
|
||||
### Error Scenarios
|
||||
- [ ] Handles self-signed certificates gracefully
|
||||
- [ ] Appropriate error messages for SSL failures
|
||||
- [ ] Fallback works when `verify_ssl: false`
|
||||
- [ ] Connection errors logged properly
|
||||
|
||||
### Configuration
|
||||
- [ ] `use_https: true` forces HTTPS URLs
|
||||
- [ ] `verify_ssl: true/false` works as expected
|
||||
- [ ] Default config uses HTTPS
|
||||
- [ ] Settings UI can modify HTTPS settings
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide for Existing Deployments
|
||||
|
||||
### Step 1: Update Configuration
|
||||
```json
|
||||
{
|
||||
"server_ip": "your-server.com",
|
||||
"port": "443",
|
||||
"use_https": true,
|
||||
"verify_ssl": true,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Restart Player Application
|
||||
```bash
|
||||
./stop_player.sh
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### Step 3: Verify Connection
|
||||
- Check logs for successful authentication
|
||||
- Verify certificate is saved: `ls ~/.kiwy-signage/`
|
||||
- Test playlist fetch works
|
||||
|
||||
### Step 4: Monitor for Issues
|
||||
- Watch for SSL-related errors in logs
|
||||
- Verify all API calls work (playlist, feedback, heartbeat)
|
||||
- Monitor player performance
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Issue:** `SSL: CERTIFICATE_VERIFY_FAILED`
|
||||
- Solution: Set `verify_ssl: false` temporarily or ensure server certificate is valid
|
||||
|
||||
**Issue:** `Connection refused` on HTTPS
|
||||
- Solution: Check HTTPS port (443) is open, verify nginx is running
|
||||
|
||||
**Issue:** Certificate endpoint not accessible
|
||||
- Solution: Ensure server has `/api/certificate` endpoint, check firewall rules
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Certificate Pinning**
|
||||
- Pin specific certificates for critical deployments
|
||||
- Prevent certificate substitution attacks
|
||||
|
||||
2. **Automatic Certificate Updates**
|
||||
- Check for certificate updates before expiration
|
||||
- Automatic renewal mechanism
|
||||
|
||||
3. **Certificate Chain Validation**
|
||||
- Validate intermediate certificates
|
||||
- Handle certificate chains properly
|
||||
|
||||
4. **Hardware Security**
|
||||
- Support for hardware security modules
|
||||
- Secure key storage on device
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The Kiwy-Signage application now fully supports HTTPS connections with:
|
||||
- ✅ Automatic SSL certificate management
|
||||
- ✅ Secure player authentication
|
||||
- ✅ HTTPS playlist fetching
|
||||
- ✅ HTTPS media file downloads
|
||||
- ✅ Configurable SSL verification
|
||||
- ✅ Comprehensive error handling
|
||||
- ✅ Development/testing modes
|
||||
|
||||
All changes follow the integration_guide.md specifications and are backward compatible with existing deployments.
|
||||
@@ -1,312 +0,0 @@
|
||||
# HTTPS Implementation Quick Reference
|
||||
|
||||
## Configuration
|
||||
|
||||
### app_config.json Settings
|
||||
|
||||
```json
|
||||
{
|
||||
"use_https": true, // Enable HTTPS connections (default: true)
|
||||
"verify_ssl": true, // Verify SSL certificates (default: true, false for dev)
|
||||
"server_ip": "your-server.com",
|
||||
"port": "443" // Use 443 for HTTPS, 5000 for HTTP
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Usage Examples
|
||||
|
||||
### 1. Authentication with HTTPS
|
||||
|
||||
```python
|
||||
from player_auth import PlayerAuth
|
||||
|
||||
# Create auth instance with HTTPS enabled
|
||||
auth = PlayerAuth(
|
||||
config_file='player_auth.json',
|
||||
use_https=True,
|
||||
verify_ssl=True
|
||||
)
|
||||
|
||||
# Authenticate with server
|
||||
success, error = auth.authenticate(
|
||||
server_url='https://your-server.com',
|
||||
hostname='player-001',
|
||||
quickconnect_code='ABC123XYZ'
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f"Connected: {auth.get_player_name()}")
|
||||
else:
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
### 2. Fetching Playlists with HTTPS
|
||||
|
||||
```python
|
||||
from get_playlists_v2 import update_playlist_if_needed
|
||||
|
||||
config = {
|
||||
'server_ip': 'your-server.com',
|
||||
'port': '443',
|
||||
'screen_name': 'player-001',
|
||||
'quickconnect_key': 'ABC123XYZ',
|
||||
'use_https': True,
|
||||
'verify_ssl': True
|
||||
}
|
||||
|
||||
# This will automatically handle HTTPS and SSL verification
|
||||
playlist_file = update_playlist_if_needed(
|
||||
config=config,
|
||||
playlist_dir='./playlists',
|
||||
media_dir='./media'
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Manual SSL Setup
|
||||
|
||||
```python
|
||||
from ssl_utils import SSLManager, setup_ssl_for_requests
|
||||
|
||||
# Option A: Use SSLManager directly
|
||||
ssl_manager = SSLManager(verify_ssl=True)
|
||||
|
||||
# Download server certificate
|
||||
success, error = ssl_manager.download_server_certificate(
|
||||
server_url='https://your-server.com'
|
||||
)
|
||||
|
||||
if success:
|
||||
# Use session for requests
|
||||
session = ssl_manager.get_session()
|
||||
response = session.get('https://your-server.com/api/data')
|
||||
|
||||
# Option B: Quick setup
|
||||
session, success = setup_ssl_for_requests(
|
||||
server_url='your-server.com',
|
||||
use_https=True,
|
||||
verify_ssl=True
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Handling SSL Errors
|
||||
|
||||
```python
|
||||
try:
|
||||
response = session.get('https://your-server.com/api/data')
|
||||
except requests.exceptions.SSLError as e:
|
||||
print(f"SSL Error: {e}")
|
||||
# Options:
|
||||
# 1. Ensure certificate is valid
|
||||
# 2. Download certificate from /api/certificate
|
||||
# 3. Set verify_ssl=False for testing only
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"Connection Error: {e}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Configuration Scenarios
|
||||
|
||||
### Scenario 1: Production with Proper Certificate
|
||||
```json
|
||||
{
|
||||
"server_ip": "production-server.com",
|
||||
"port": "443",
|
||||
"use_https": true,
|
||||
"verify_ssl": true
|
||||
}
|
||||
```
|
||||
✓ Most secure, requires valid certificate from trusted CA
|
||||
|
||||
### Scenario 2: Self-Signed Certificate (Test)
|
||||
```json
|
||||
{
|
||||
"server_ip": "test-server.local",
|
||||
"port": "443",
|
||||
"use_https": true,
|
||||
"verify_ssl": true
|
||||
}
|
||||
```
|
||||
- First run: certificate will be downloaded automatically
|
||||
- Subsequent runs: saved certificate will be used
|
||||
|
||||
### Scenario 3: Development Mode (No SSL)
|
||||
```json
|
||||
{
|
||||
"server_ip": "localhost",
|
||||
"port": "5000",
|
||||
"use_https": false,
|
||||
"verify_ssl": false
|
||||
}
|
||||
```
|
||||
⚠️ Not secure - development only!
|
||||
|
||||
### Scenario 4: HTTPS with No Verification (Testing)
|
||||
```json
|
||||
{
|
||||
"server_ip": "test-server.local",
|
||||
"port": "443",
|
||||
"use_https": true,
|
||||
"verify_ssl": false
|
||||
}
|
||||
```
|
||||
⚠️ Insecure - testing only!
|
||||
|
||||
---
|
||||
|
||||
## Certificate Management
|
||||
|
||||
### View Saved Certificate Info
|
||||
```python
|
||||
from ssl_utils import SSLManager
|
||||
|
||||
ssl_mgr = SSLManager()
|
||||
cert_info = ssl_mgr.get_certificate_info()
|
||||
print(cert_info)
|
||||
# Output: {
|
||||
# 'subject': '...',
|
||||
# 'issuer': '...',
|
||||
# 'valid_from': '...',
|
||||
# 'valid_until': '...',
|
||||
# 'fingerprint': '...'
|
||||
# }
|
||||
```
|
||||
|
||||
### Re-download Certificate
|
||||
```python
|
||||
from ssl_utils import SSLManager
|
||||
|
||||
ssl_mgr = SSLManager()
|
||||
success, error = ssl_mgr.download_server_certificate(
|
||||
server_url='https://your-server.com'
|
||||
)
|
||||
|
||||
if success:
|
||||
print("✓ Certificate updated")
|
||||
else:
|
||||
print(f"✗ Failed: {error}")
|
||||
```
|
||||
|
||||
### Certificate Location
|
||||
```
|
||||
~/.kiwy-signage/
|
||||
├── server_cert.pem # The actual certificate
|
||||
└── cert_info.json # Certificate metadata
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: `SSL: CERTIFICATE_VERIFY_FAILED`
|
||||
|
||||
**Cause:** Certificate validation failed
|
||||
|
||||
**Solutions:**
|
||||
1. Ensure server certificate is valid:
|
||||
```bash
|
||||
openssl s_client -connect your-server.com:443
|
||||
```
|
||||
|
||||
2. For self-signed certs, let player download it:
|
||||
- First connection will attempt download from `/api/certificate`
|
||||
- Subsequent connections use saved cert
|
||||
|
||||
3. Temporarily disable verification (testing only):
|
||||
```json
|
||||
{"verify_ssl": false}
|
||||
```
|
||||
|
||||
### Problem: `Connection refused` on HTTPS
|
||||
|
||||
**Cause:** HTTPS port (443) not accessible
|
||||
|
||||
**Solutions:**
|
||||
1. Verify HTTPS is enabled on server
|
||||
2. Check firewall rules allow port 443
|
||||
3. Verify nginx/server is running:
|
||||
```bash
|
||||
netstat -tuln | grep 443
|
||||
```
|
||||
|
||||
### Problem: Certificate endpoint returns 404
|
||||
|
||||
**Cause:** `/api/certificate` endpoint not available
|
||||
|
||||
**Solutions:**
|
||||
1. Verify server has certificate endpoint implemented
|
||||
2. Check server URL is correct
|
||||
3. Ensure CORS is enabled (if cross-origin)
|
||||
|
||||
### Problem: Slow HTTPS connections
|
||||
|
||||
**Possible Causes:**
|
||||
1. SSL handshake timeout - increase timeout:
|
||||
```python
|
||||
auth.authenticate(..., timeout=60)
|
||||
```
|
||||
|
||||
2. Certificate revocation check - disable if not needed:
|
||||
- Not controlled by app, check system settings
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
- [ ] Update `app_config.json` with `use_https: true`
|
||||
- [ ] Update `port` to 443 (if HTTPS)
|
||||
- [ ] Verify server has valid HTTPS certificate
|
||||
- [ ] Test connection in settings UI
|
||||
- [ ] Monitor logs for SSL errors
|
||||
- [ ] Verify certificate is saved: `ls ~/.kiwy-signage/`
|
||||
- [ ] Test playlist fetch works
|
||||
- [ ] Test media downloads work
|
||||
- [ ] Test status feedback works
|
||||
|
||||
---
|
||||
|
||||
## Debug Logging
|
||||
|
||||
Enable detailed logging for debugging HTTPS issues:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
# Enable debug logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger('ssl_utils')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Now run your code and check logs
|
||||
auth = PlayerAuth(use_https=True, verify_ssl=True)
|
||||
auth.authenticate(...)
|
||||
```
|
||||
|
||||
Look for messages like:
|
||||
- `Using saved certificate: ~/.kiwy-signage/server_cert.pem`
|
||||
- `SSL context configured with server certificate`
|
||||
- `SSL Certificate saved to...`
|
||||
- `SSL Error: ...` (if there are issues)
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/ssl_utils.py` | SSL/HTTPS utilities and certificate management |
|
||||
| `src/player_auth.py` | Player authentication with HTTPS support |
|
||||
| `src/get_playlists_v2.py` | Playlist fetching with HTTPS |
|
||||
| `src/main.py` | Main app with HTTPS configuration |
|
||||
| `config/app_config.json` | Configuration with HTTPS settings |
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [integration_guide.md](integration_guide.md) - Full server-side requirements
|
||||
- [HTTPS_IMPLEMENTATION.md](HTTPS_IMPLEMENTATION.md) - Detailed implementation guide
|
||||
- [SSL Certificate Files](~/.kiwy-signage/) - Local certificate storage
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
# Implementation Complete: HTTPS Support for Kiwy-Signage
|
||||
|
||||
## Status: ✅ COMPLETE
|
||||
|
||||
All changes from `integration_guide.md` have been successfully implemented into the Kiwy-Signage application.
|
||||
|
||||
---
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
### New Files Created
|
||||
|
||||
1. **`src/ssl_utils.py`** - Complete SSL/HTTPS utilities module
|
||||
- SSLManager class for certificate handling
|
||||
- Automatic certificate download and storage
|
||||
- SSL-configured requests session management
|
||||
- Certificate validation and info retrieval
|
||||
|
||||
### Modified Files
|
||||
|
||||
2. **`src/player_auth.py`** - Enhanced with HTTPS support
|
||||
- SSL manager integration
|
||||
- HTTPS-aware authentication
|
||||
- SSL error handling
|
||||
- All API methods updated to use SSL sessions
|
||||
|
||||
3. **`src/get_playlists_v2.py`** - HTTPS playlist management
|
||||
- HTTPS configuration support
|
||||
- SSL manager for media downloads
|
||||
- Enhanced error handling for SSL issues
|
||||
|
||||
4. **`src/main.py`** - Configuration and UI updates
|
||||
- Default config now uses HTTPS (port 443)
|
||||
- Connection test passes HTTPS settings
|
||||
- Better logging for SSL connections
|
||||
|
||||
5. **`config/app_config.json`** - Configuration update
|
||||
- Added `"use_https": true`
|
||||
- Added `"verify_ssl": true`
|
||||
- Port explicitly set to 443
|
||||
|
||||
### Documentation Created
|
||||
|
||||
6. **`HTTPS_IMPLEMENTATION.md`** - Complete implementation guide
|
||||
- Detailed file-by-file changes
|
||||
- SSL certificate flow explanation
|
||||
- Security considerations
|
||||
- Testing checklist
|
||||
- Migration guide
|
||||
|
||||
7. **`HTTPS_QUICK_REFERENCE.md`** - Developer quick reference
|
||||
- Code usage examples
|
||||
- Configuration scenarios
|
||||
- Troubleshooting guide
|
||||
- Certificate management commands
|
||||
|
||||
---
|
||||
|
||||
## Key Features Implemented
|
||||
|
||||
### ✅ Automatic Certificate Management
|
||||
- Player automatically downloads server certificate on first connection
|
||||
- Certificate stored locally in `~/.kiwy-signage/`
|
||||
- Subsequent connections use saved certificate
|
||||
|
||||
### ✅ Secure Authentication
|
||||
- All authentication now uses HTTPS
|
||||
- Automatic URL scheme normalization to HTTPS
|
||||
- SSL certificate verification (configurable)
|
||||
|
||||
### ✅ HTTPS Playlist Operations
|
||||
- Playlist fetching over HTTPS
|
||||
- Media file downloads over HTTPS
|
||||
- Status feedback via HTTPS
|
||||
|
||||
### ✅ Configurable Security
|
||||
- `use_https` setting to enable/disable HTTPS
|
||||
- `verify_ssl` setting for certificate verification
|
||||
- Development mode support (without verification)
|
||||
|
||||
### ✅ Robust Error Handling
|
||||
- SSL-specific error messages
|
||||
- Graceful fallbacks
|
||||
- Comprehensive logging
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Minimal Setup (Using Defaults)
|
||||
```json
|
||||
{
|
||||
"server_ip": "digi-signage.moto-adv.com",
|
||||
"port": "443",
|
||||
"screen_name": "tv-terasa",
|
||||
"quickconnect_key": "8887779",
|
||||
"use_https": true,
|
||||
"verify_ssl": true
|
||||
}
|
||||
```
|
||||
|
||||
### For Testing (Without SSL Verification)
|
||||
```json
|
||||
{
|
||||
"use_https": true,
|
||||
"verify_ssl": false
|
||||
}
|
||||
```
|
||||
|
||||
### For HTTP (Development Only)
|
||||
```json
|
||||
{
|
||||
"use_https": false,
|
||||
"verify_ssl": false,
|
||||
"port": "5000"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing & Verification
|
||||
|
||||
### ✅ Syntax Validation
|
||||
- All Python files compile without errors
|
||||
- All JSON configurations are valid
|
||||
- No import errors
|
||||
|
||||
### ✅ Integration Points
|
||||
- Player authentication with HTTPS ✓
|
||||
- Playlist fetching with HTTPS ✓
|
||||
- Media downloads with HTTPS ✓
|
||||
- Status feedback via HTTPS ✓
|
||||
- Certificate management ✓
|
||||
|
||||
### ✅ Backward Compatibility
|
||||
- Existing HTTP deployments still work (`use_https: false`)
|
||||
- Legacy configuration loading still supported
|
||||
- All changes are non-breaking
|
||||
|
||||
---
|
||||
|
||||
## Deployment Instructions
|
||||
|
||||
### Step 1: Update Configuration
|
||||
Edit `config/app_config.json` and ensure:
|
||||
```json
|
||||
{
|
||||
"use_https": true,
|
||||
"verify_ssl": true,
|
||||
"port": "443"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Restart Application
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage
|
||||
./stop_player.sh
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### Step 3: Verify Functionality
|
||||
- Monitor logs for SSL messages
|
||||
- Check certificate is saved: `ls ~/.kiwy-signage/`
|
||||
- Test playlist fetch works
|
||||
- Confirm all API calls succeed
|
||||
|
||||
### Step 4: Monitor
|
||||
- Watch for SSL-related errors in first hours
|
||||
- Verify performance is acceptable
|
||||
- Monitor certificate expiration if applicable
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Quick Links
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| `SSL: CERTIFICATE_VERIFY_FAILED` | See HTTPS_QUICK_REFERENCE.md - Troubleshooting |
|
||||
| Connection refused on 443 | Check HTTPS is enabled on server |
|
||||
| Certificate endpoint 404 | Verify `/api/certificate` exists on server |
|
||||
| Slow HTTPS | Increase timeout in player_auth.py |
|
||||
|
||||
See `HTTPS_QUICK_REFERENCE.md` for detailed troubleshooting.
|
||||
|
||||
---
|
||||
|
||||
## Files Modified Summary
|
||||
|
||||
| File | Changes | Status |
|
||||
|------|---------|--------|
|
||||
| src/ssl_utils.py | NEW - SSL utilities | ✅ Created |
|
||||
| src/player_auth.py | HTTPS support added | ✅ Updated |
|
||||
| src/get_playlists_v2.py | HTTPS downloads | ✅ Updated |
|
||||
| src/main.py | Config & UI | ✅ Updated |
|
||||
| config/app_config.json | HTTPS settings | ✅ Updated |
|
||||
| HTTPS_IMPLEMENTATION.md | NEW - Full guide | ✅ Created |
|
||||
| HTTPS_QUICK_REFERENCE.md | NEW - Quick ref | ✅ Created |
|
||||
|
||||
---
|
||||
|
||||
## Compliance with integration_guide.md
|
||||
|
||||
- ✅ Python/Requests library certificate handling implemented
|
||||
- ✅ SSL certificate endpoint integration ready
|
||||
- ✅ Environment configuration supports HTTPS
|
||||
- ✅ HTTPS-friendly proxy configuration ready for server
|
||||
- ✅ Testing checklist included
|
||||
- ✅ Migration steps documented
|
||||
- ✅ Troubleshooting guide provided
|
||||
- ✅ Security recommendations incorporated
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Server Setup:** Ensure server has `/api/certificate` endpoint
|
||||
2. **Testing:** Run through testing checklist in HTTPS_IMPLEMENTATION.md
|
||||
3. **Deployment:** Follow deployment instructions above
|
||||
4. **Monitoring:** Watch logs for any SSL-related issues
|
||||
5. **Documentation:** Share HTTPS_QUICK_REFERENCE.md with operators
|
||||
|
||||
---
|
||||
|
||||
## Support & Documentation
|
||||
|
||||
- **Full Implementation Guide:** `HTTPS_IMPLEMENTATION.md`
|
||||
- **Quick Reference:** `HTTPS_QUICK_REFERENCE.md`
|
||||
- **Server Integration:** `integration_guide.md`
|
||||
- **Source Code:** `src/ssl_utils.py`, `src/player_auth.py`, `src/get_playlists_v2.py`
|
||||
|
||||
---
|
||||
|
||||
## Version Info
|
||||
|
||||
- **Implementation Date:** January 16, 2026
|
||||
- **Based On:** integration_guide.md specifications
|
||||
- **Python Version:** 3.7+
|
||||
- **Framework:** Kivy 2.3.1
|
||||
|
||||
---
|
||||
|
||||
**Implementation Status: READY FOR PRODUCTION** ✅
|
||||
|
||||
All features from the integration guide have been implemented and tested.
|
||||
The application is now compatible with HTTPS servers.
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
# Player Code HTTPS Integration Guide
|
||||
|
||||
## Server-Side Improvements Implemented
|
||||
|
||||
All critical and medium improvements have been implemented on the server:
|
||||
|
||||
### ✅ CORS Support Enabled
|
||||
- **File**: `app/extensions.py` - CORS extension initialized
|
||||
- **File**: `app/app.py` - CORS configured for `/api/*` endpoints
|
||||
- All player API requests now support cross-origin requests
|
||||
- Preflight OPTIONS requests are properly handled
|
||||
|
||||
### ✅ SSL Certificate Endpoint Added
|
||||
- **Endpoint**: `GET /api/certificate`
|
||||
- **Location**: `app/blueprints/api.py`
|
||||
- Returns server certificate in PEM format with metadata:
|
||||
- Certificate content (PEM format)
|
||||
- Certificate info (subject, issuer, validity dates, fingerprint)
|
||||
- Integration instructions for different platforms
|
||||
|
||||
### ✅ HTTPS Configuration Updated
|
||||
- **File**: `app/config.py` - ProductionConfig now has:
|
||||
- `SESSION_COOKIE_SECURE = True`
|
||||
- `SESSION_COOKIE_SAMESITE = 'Lax'`
|
||||
- **File**: `nginx.conf` - Added:
|
||||
- CORS headers for all responses
|
||||
- OPTIONS request handling
|
||||
- X-Forwarded-Port header forwarding
|
||||
|
||||
### ✅ Nginx Proxy Configuration Enhanced
|
||||
- Added CORS headers at nginx level for defense-in-depth
|
||||
- Proper X-Forwarded headers for protocol/port detection
|
||||
- HTTPS-friendly proxy configuration
|
||||
|
||||
---
|
||||
|
||||
## Required Player Code Changes
|
||||
|
||||
### 1. **For Python/Kivy Players Using Requests Library**
|
||||
|
||||
**Update:** Import and use certificate handling:
|
||||
|
||||
```python
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
import os
|
||||
|
||||
class DigiServerClient:
|
||||
def __init__(self, server_url, hostname, quickconnect_code, use_https=True):
|
||||
self.server_url = server_url
|
||||
self.hostname = hostname
|
||||
self.quickconnect_code = quickconnect_code
|
||||
self.session = requests.Session()
|
||||
|
||||
# CRITICAL: Handle SSL verification
|
||||
if use_https:
|
||||
# Option 1: Get certificate from server and trust it
|
||||
self.setup_certificate_trust()
|
||||
else:
|
||||
# Option 2: Disable SSL verification (DEV ONLY)
|
||||
self.session.verify = False
|
||||
|
||||
def setup_certificate_trust(self):
|
||||
"""Download server certificate and configure trust."""
|
||||
try:
|
||||
# First, make a request without verification to get the cert
|
||||
response = requests.get(
|
||||
f"{self.server_url}/api/certificate",
|
||||
verify=False,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
cert_data = response.json()
|
||||
|
||||
# Save certificate locally
|
||||
cert_path = os.path.expanduser('~/.digiserver/server_cert.pem')
|
||||
os.makedirs(os.path.dirname(cert_path), exist_ok=True)
|
||||
|
||||
with open(cert_path, 'w') as f:
|
||||
f.write(cert_data['certificate'])
|
||||
|
||||
# Configure session to use this certificate
|
||||
self.session.verify = cert_path
|
||||
|
||||
print(f"✓ Server certificate installed from {cert_data['certificate_info']['issuer']}")
|
||||
print(f" Valid until: {cert_data['certificate_info']['valid_until']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to setup certificate trust: {e}")
|
||||
print(" Falling back to unverified connection (not recommended for production)")
|
||||
self.session.verify = False
|
||||
|
||||
def get_playlist(self):
|
||||
"""Get playlist from server with proper error handling."""
|
||||
try:
|
||||
response = self.session.get(
|
||||
f"{self.server_url}/api/playlists",
|
||||
params={
|
||||
'hostname': self.hostname,
|
||||
'quickconnect_code': self.quickconnect_code
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
except requests.exceptions.SSLError as e:
|
||||
print(f"❌ SSL Error: {e}")
|
||||
# Log error for debugging
|
||||
print(" This usually means the server certificate is not trusted.")
|
||||
print(" Try running: DigiServerClient.setup_certificate_trust()")
|
||||
raise
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"❌ Connection Error: {e}")
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
raise
|
||||
|
||||
def send_feedback(self, status, message=''):
|
||||
"""Send player feedback/status to server."""
|
||||
try:
|
||||
response = self.session.post(
|
||||
f"{self.server_url}/api/player-feedback",
|
||||
json={
|
||||
'hostname': self.hostname,
|
||||
'quickconnect_code': self.quickconnect_code,
|
||||
'status': status,
|
||||
'message': message,
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"Error sending feedback: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
### 2. **For Kivy Framework Specifically**
|
||||
|
||||
**Update:** In your Kivy HTTP client configuration:
|
||||
|
||||
```python
|
||||
from kivy.network.urlrequest import UrlRequest
|
||||
from kivy.logger import Logger
|
||||
import ssl
|
||||
import certifi
|
||||
|
||||
class DigiServerKivyClient:
|
||||
def __init__(self, server_url, hostname, quickconnect_code):
|
||||
self.server_url = server_url
|
||||
self.hostname = hostname
|
||||
self.quickconnect_code = quickconnect_code
|
||||
|
||||
# Configure SSL context for Kivy requests
|
||||
self.ssl_context = self._setup_ssl_context()
|
||||
|
||||
def _setup_ssl_context(self):
|
||||
"""Setup SSL context with certificate trust."""
|
||||
try:
|
||||
# Try to get server certificate
|
||||
import requests
|
||||
response = requests.get(
|
||||
f"{self.server_url}/api/certificate",
|
||||
verify=False,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
cert_data = response.json()
|
||||
cert_path = os._get_cert_path()
|
||||
|
||||
with open(cert_path, 'w') as f:
|
||||
f.write(cert_data['certificate'])
|
||||
|
||||
# Create SSL context
|
||||
context = ssl.create_default_context()
|
||||
context.load_verify_locations(cert_path)
|
||||
|
||||
Logger.info('DigiServer', f'SSL context configured with server certificate')
|
||||
return context
|
||||
|
||||
except Exception as e:
|
||||
Logger.warning('DigiServer', f'Failed to setup SSL: {e}')
|
||||
return None
|
||||
|
||||
def fetch_playlist(self, callback):
|
||||
"""Fetch playlist with proper SSL handling."""
|
||||
url = f"{self.server_url}/api/playlists"
|
||||
params = f"?hostname={self.hostname}&quickconnect_code={self.quickconnect_code}"
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Kiwy-Signage-Player/1.0'
|
||||
}
|
||||
|
||||
request = UrlRequest(
|
||||
url + params,
|
||||
on_success=callback,
|
||||
on_error=self._on_error,
|
||||
on_failure=self._on_failure,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
return request
|
||||
|
||||
def _on_error(self, request, error):
|
||||
Logger.error('DigiServer', f'Request error: {error}')
|
||||
|
||||
def _on_failure(self, request, result):
|
||||
Logger.error('DigiServer', f'Request failed: {result}')
|
||||
```
|
||||
|
||||
### 3. **Environment Configuration**
|
||||
|
||||
**Add to player app_config.json or environment:**
|
||||
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"url": "https://192.168.0.121",
|
||||
"hostname": "player1",
|
||||
"quickconnect_code": "ABC123XYZ",
|
||||
"verify_ssl": false,
|
||||
"use_server_certificate": true,
|
||||
"certificate_path": "~/.digiserver/server_cert.pem"
|
||||
},
|
||||
"connection": {
|
||||
"timeout": 10,
|
||||
"retry_attempts": 3,
|
||||
"retry_delay": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Server-Side Tests
|
||||
|
||||
- [ ] Verify CORS headers present: `curl -v https://192.168.0.121/api/health`
|
||||
- [ ] Check certificate endpoint: `curl -k https://192.168.0.121/api/certificate`
|
||||
- [ ] Test OPTIONS preflight: `curl -X OPTIONS https://192.168.0.121/api/playlists`
|
||||
- [ ] Verify X-Forwarded headers: `curl -v https://192.168.0.121/`
|
||||
|
||||
### Player Connection Tests
|
||||
|
||||
- [ ] Player connects with HTTPS successfully
|
||||
- [ ] Player fetches playlist without SSL errors
|
||||
- [ ] Player receives status update confirmation
|
||||
- [ ] Player sends feedback/heartbeat correctly
|
||||
|
||||
### Integration Tests
|
||||
|
||||
```bash
|
||||
# Test certificate retrieval
|
||||
curl -k https://192.168.0.121/api/certificate | jq '.certificate_info'
|
||||
|
||||
# Test CORS preflight for player
|
||||
curl -X OPTIONS https://192.168.0.121/api/playlists \
|
||||
-H "Origin: http://192.168.0.121" \
|
||||
-H "Access-Control-Request-Method: GET" \
|
||||
-v
|
||||
|
||||
# Simulate player playlist fetch
|
||||
curl -k https://192.168.0.121/api/playlists \
|
||||
--data-urlencode "hostname=test-player" \
|
||||
--data-urlencode "quickconnect_code=test123" \
|
||||
-H "Origin: *"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### For Existing Players
|
||||
|
||||
1. **Update player code** with new SSL handling from this guide
|
||||
2. **Restart player application** to pick up changes
|
||||
3. **Verify connection** works with HTTPS server
|
||||
4. **Monitor logs** for any SSL-related errors
|
||||
|
||||
### For New Players
|
||||
|
||||
1. **Deploy updated player code** with SSL support from the start
|
||||
2. **Configure with HTTPS server URL**
|
||||
3. **Run initialization** to fetch and trust server certificate
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "SSL: CERTIFICATE_VERIFY_FAILED"
|
||||
- Player is rejecting the self-signed certificate
|
||||
- **Solution**: Run certificate trust setup or disable SSL verification
|
||||
|
||||
### "Connection Refused"
|
||||
- Server HTTPS port not accessible
|
||||
- **Solution**: Check nginx is running, port 443 is open, firewall rules
|
||||
|
||||
### "CORS error"
|
||||
- Browser/HTTP client blocking cross-origin request
|
||||
- **Solution**: Verify CORS headers in response, check Origin header
|
||||
|
||||
### "Certificate not found at endpoint"
|
||||
- Server certificate file missing
|
||||
- **Solution**: Verify cert.pem exists at `/etc/nginx/ssl/cert.pem`
|
||||
|
||||
---
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
1. **For Development/Testing**: Disable SSL verification temporarily
|
||||
```python
|
||||
session.verify = False
|
||||
```
|
||||
|
||||
2. **For Production**:
|
||||
- Use proper certificates (Let's Encrypt recommended)
|
||||
- Deploy certificate trust setup at player initialization
|
||||
- Monitor SSL certificate expiration
|
||||
- Implement certificate pinning for critical deployments
|
||||
|
||||
3. **For Self-Signed Certificates**:
|
||||
- Use `/api/certificate` endpoint to distribute certificates
|
||||
- Store certificates in secure location on device
|
||||
- Implement certificate update mechanism
|
||||
- Log certificate trust changes for auditing
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Implement SSL handling** in player code using examples above
|
||||
2. **Test with HTTP first** to ensure API works
|
||||
3. **Enable HTTPS** and test with certificate handling
|
||||
4. **Deploy to production** with proper SSL setup
|
||||
5. **Monitor** player connections and SSL errors
|
||||
|
||||
@@ -51,6 +51,18 @@ they remain available on the `Windows-Player` branch.
|
||||
* `.github/instructions/kiwy-build-and-development.instructions.md` (the exe build guide)
|
||||
* `documentation/CODE_SIGNING_SMART_APP_CONTROL.md` (Windows-only signing constraint)
|
||||
|
||||
**Removed later: the rest of the Windows-era leftovers** (all recoverable from the
|
||||
`Windows-Player` branch, so nothing was lost):
|
||||
|
||||
| Item | Why it went |
|
||||
|------|-------------|
|
||||
| `working_files/` (29 files) | Dev-era scratch: `install.sh.bak`, the superseded `get_playlists.py` (v1, replaced by `get_playlists_v2.py`), one-off `test_*.py` probes, and `MIGRATION_GUIDE.md` / `INVESTIGATION_RESULTS.md`. Also held real server captures (`server_response_debug.json`: `player_id`, player name, playlist) which had no business in the repo. |
|
||||
| `documentation/` (5 files) | All described the Windows-era HTTPS integration work. |
|
||||
| `python version` | Contained `Python 3.12.9` — the Windows build interpreter. The Pi runs 3.13.5. |
|
||||
| `test_edited_media_upload.py` | Parentless debug script, referenced by nothing. |
|
||||
| `.display-keepalive.sh`, `.keep-screen-alive.sh`, `.wait-for-display.sh` | Orphan X11 helper scripts, referenced by nothing. Superseded by `linux/linux_display.py`. |
|
||||
| `.video-optimization.sh`, `.run-background.sh`, `.start-player-cron.sh` | **Kept** — `install.sh` references all three. |
|
||||
|
||||
**Removed from shared `src/`** — these were live code paths, so this was a real
|
||||
behavioural change, not just a comment cleanup:
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Python 3.12.9
|
||||
@@ -1,165 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to diagnose edited media upload issues
|
||||
Run this to test if the server endpoint exists and works correctly
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# Add src to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
def test_upload():
|
||||
"""Test the edited media upload functionality"""
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("EDITED MEDIA UPLOAD DIAGNOSTICS")
|
||||
print("="*60)
|
||||
|
||||
# Get authentication
|
||||
try:
|
||||
from get_playlists_v2 import get_auth_instance
|
||||
auth = get_auth_instance()
|
||||
|
||||
if not auth or not auth.is_authenticated():
|
||||
print("❌ ERROR: Not authenticated!")
|
||||
print(" Please ensure player_auth.json exists and is valid")
|
||||
return False
|
||||
|
||||
server_url = auth.auth_data.get('server_url')
|
||||
auth_code = auth.auth_data.get('auth_code')
|
||||
|
||||
print(f"\n✓ Authentication successful")
|
||||
print(f" Server URL: {server_url}")
|
||||
print(f" Auth Code: {auth_code[:20]}..." if auth_code else " Auth Code: None")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Authentication error: {e}")
|
||||
return False
|
||||
|
||||
# Check for edited media files
|
||||
edited_media_dir = os.path.join(os.path.dirname(__file__), 'media', 'edited_media')
|
||||
edited_files = list(Path(edited_media_dir).glob('*_e_v*.jpg'))
|
||||
metadata_files = list(Path(edited_media_dir).glob('*_metadata.json'))
|
||||
|
||||
print(f"\n✓ Edited Media Directory: {edited_media_dir}")
|
||||
print(f" Edited images: {len(edited_files)}")
|
||||
print(f" Metadata files: {len(metadata_files)}")
|
||||
|
||||
if not edited_files:
|
||||
print("\n⚠️ No edited images found!")
|
||||
print(" Create an edit first, then run this test")
|
||||
return False
|
||||
|
||||
# Test with the first edited image
|
||||
image_path = str(edited_files[0])
|
||||
metadata_file = str(edited_files[0]).replace('.jpg', '_metadata.json')
|
||||
|
||||
if not os.path.exists(metadata_file):
|
||||
print(f"\n❌ Metadata file not found: {metadata_file}")
|
||||
return False
|
||||
|
||||
print(f"\nTesting upload with:")
|
||||
print(f" Image: {os.path.basename(image_path)}")
|
||||
print(f" Size: {os.path.getsize(image_path):,} bytes")
|
||||
print(f" Metadata: {os.path.basename(metadata_file)}")
|
||||
|
||||
# Load and display metadata
|
||||
with open(metadata_file, 'r') as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
print(f"\nMetadata content:")
|
||||
for key, value in metadata.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
# Add original_filename if not present
|
||||
metadata['original_filename'] = os.path.basename(metadata['original_path'])
|
||||
|
||||
# Prepare upload request
|
||||
upload_url = f"{server_url}/api/player-edit-media"
|
||||
headers = {'Authorization': f'Bearer {auth_code}'}
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("TESTING UPLOAD...")
|
||||
print(f"{'='*60}")
|
||||
print(f"Endpoint: {upload_url}")
|
||||
print(f"Headers: Authorization: Bearer {auth_code[:20]}...")
|
||||
|
||||
try:
|
||||
with open(image_path, 'rb') as img_file:
|
||||
files = {
|
||||
'image_file': (metadata['original_filename'], img_file, 'image/jpeg')
|
||||
}
|
||||
data = {
|
||||
'metadata': json.dumps(metadata),
|
||||
'original_file': metadata['original_filename']
|
||||
}
|
||||
|
||||
print(f"\nSending request (30s timeout, SSL verify=False)...")
|
||||
response = requests.post(
|
||||
upload_url,
|
||||
headers=headers,
|
||||
files=files,
|
||||
data=data,
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
|
||||
print(f"\n✓ Response received!")
|
||||
print(f" Status Code: {response.status_code}")
|
||||
print(f" Headers: {dict(response.headers)}")
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"\n✅ SUCCESS! Server accepted the upload")
|
||||
print(f" Response: {response.json()}")
|
||||
return True
|
||||
elif response.status_code == 404:
|
||||
print(f"\n❌ ENDPOINT NOT FOUND (404)")
|
||||
print(f" The server does NOT have /api/player-edit-media endpoint")
|
||||
print(f" Server may need to implement this feature")
|
||||
elif response.status_code == 401:
|
||||
print(f"\n❌ AUTHENTICATION FAILED (401)")
|
||||
print(f" Check your auth_code in player_auth.json")
|
||||
else:
|
||||
print(f"\n❌ REQUEST FAILED (Status: {response.status_code})")
|
||||
print(f" Response: {response.text}")
|
||||
|
||||
return False
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"\n❌ CONNECTION ERROR")
|
||||
print(f" Cannot reach server at {server_url}")
|
||||
print(f" Error: {e}")
|
||||
return False
|
||||
except requests.exceptions.Timeout as e:
|
||||
print(f"\n❌ TIMEOUT")
|
||||
print(f" Server did not respond within 30 seconds")
|
||||
print(f" Error: {e}")
|
||||
return False
|
||||
except requests.exceptions.SSLError as e:
|
||||
print(f"\n❌ SSL ERROR")
|
||||
print(f" Error: {e}")
|
||||
print(f" Tip: Try adding verify=False to requests")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"\n❌ UNEXPECTED ERROR")
|
||||
print(f" Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = test_upload()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
if success:
|
||||
print("✅ UPLOAD TEST PASSED - Server accepts edited media!")
|
||||
else:
|
||||
print("❌ UPLOAD TEST FAILED - See details above")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -1,182 +0,0 @@
|
||||
# USB Card Reader Authentication
|
||||
|
||||
This document describes the USB card reader authentication feature for the Kiwy Signage Player.
|
||||
|
||||
## Overview
|
||||
|
||||
The player now supports user authentication via USB card readers when accessing the edit/drawing interface. When a user clicks the edit button (pencil icon), they must swipe their card to authenticate before being allowed to edit the image.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Edit Button Click**: User clicks the pencil icon to edit the current image
|
||||
2. **Validation Checks**:
|
||||
- Verify current media is an image (not video)
|
||||
- Check if editing is allowed for this media (`edit_on_player` permission from server)
|
||||
3. **Card Reader Prompt**:
|
||||
- Display "Please swipe your card..." message
|
||||
- Wait for card swipe (5 second timeout)
|
||||
- Read card data from USB card reader
|
||||
- Store the card data (no validation required)
|
||||
4. **Open Edit Interface**: Edit interface opens with card data stored
|
||||
5. **Save & Upload**: When user saves the edited image:
|
||||
- Card data is included in the metadata JSON
|
||||
- Both image and metadata (with card data) are uploaded to server
|
||||
- Server receives `user_card_data` field for tracking who edited the image
|
||||
|
||||
## Card Reader Setup
|
||||
|
||||
### Hardware Requirements
|
||||
- USB card reader (HID/keyboard emulation type)
|
||||
- Compatible cards (magnetic stripe or RFID depending on reader)
|
||||
|
||||
### Software Requirements
|
||||
The player requires the `evdev` Python library to interface with USB input devices:
|
||||
|
||||
```bash
|
||||
# Install via apt (recommended for Raspberry Pi)
|
||||
sudo apt-get install python3-evdev
|
||||
|
||||
# Or via pip
|
||||
pip3 install evdev
|
||||
```
|
||||
|
||||
### Fallback Mode
|
||||
If `evdev` is not available, the player will:
|
||||
- Log a warning message
|
||||
- Use a default card value (`DEFAULT_USER_12345`) for testing
|
||||
- This allows development and testing without hardware
|
||||
|
||||
## Card Data Storage
|
||||
|
||||
The card data is captured as a raw string and stored without validation or mapping:
|
||||
|
||||
- **No preprocessing**: Card data is stored exactly as received from the reader
|
||||
- **Format**: Whatever the card reader sends (typically numeric or alphanumeric)
|
||||
- **Sent to server**: Raw card data is included in the `user_card_data` field of the metadata JSON
|
||||
- **Server-side processing**: The server can validate, map, or process the card data as needed
|
||||
|
||||
### Metadata JSON Format
|
||||
When an image is saved, the metadata includes:
|
||||
```json
|
||||
{
|
||||
"time_of_modification": "2025-12-08T10:30:00",
|
||||
"original_name": "image.jpg",
|
||||
"new_name": "image_e_v1.jpg",
|
||||
"original_path": "/path/to/image.jpg",
|
||||
"version": 1,
|
||||
"user_card_data": "123456789"
|
||||
}
|
||||
```
|
||||
|
||||
If no card is swiped (timeout), `user_card_data` will be `null`.
|
||||
|
||||
## Testing the Card Reader
|
||||
|
||||
A test utility is provided to verify card reader functionality:
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/working_files
|
||||
python3 test_card_reader.py
|
||||
```
|
||||
|
||||
The test tool will:
|
||||
1. List all available input devices
|
||||
2. Auto-detect the card reader (or let you select manually)
|
||||
3. Listen for card swipes and display the data received
|
||||
4. Show how the data will be processed
|
||||
|
||||
### Test Output Example
|
||||
```
|
||||
✓ Card data received: '123456789'
|
||||
Length: 9 characters
|
||||
Processed ID: card_123456789
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Main Components
|
||||
|
||||
1. **CardReader Class** (`main.py`)
|
||||
- Handles USB device detection
|
||||
- Reads input events from card reader
|
||||
- Provides async callback interface
|
||||
- Includes timeout handling (5 seconds)
|
||||
|
||||
2. **Card Read Flow** (`show_edit_interface()` method)
|
||||
- Validates media type and permissions
|
||||
- Initiates card read
|
||||
- Stores raw card data
|
||||
- Opens edit popup
|
||||
|
||||
3. **Metadata Creation** (`_save_metadata()` method)
|
||||
- Includes card data in metadata JSON
|
||||
- No processing or validation of card data
|
||||
- Sent to server as-is
|
||||
|
||||
### Card Data Format
|
||||
|
||||
Card readers typically send data as keyboard input:
|
||||
- Each character is sent as a key press event
|
||||
- Data ends with an ENTER key press
|
||||
- Reader format: `[CARD_DATA][ENTER]`
|
||||
|
||||
The CardReader class:
|
||||
- Captures key press events
|
||||
- Builds the card data string character by character
|
||||
- Completes reading when ENTER is detected
|
||||
- Returns the complete card data to the callback
|
||||
|
||||
### Security Considerations
|
||||
|
||||
1. **Server-Side Validation**: Card validation should be implemented on the server
|
||||
2. **Timeout**: 5-second timeout prevents infinite waiting for card swipe
|
||||
3. **Logging**: All card reads are logged with the raw card data
|
||||
4. **Permissions**: Edit permission must be enabled on the server (`edit_on_player`)
|
||||
5. **Raw Data**: Card data is sent as-is; server is responsible for validation and authorization
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Card Reader Not Detected
|
||||
- Check USB connection
|
||||
- Run `ls /dev/input/` to see available devices
|
||||
- Run the test script to verify detection
|
||||
- Check `evdev` is installed: `python3 -c "import evdev"`
|
||||
|
||||
### Card Swipes Not Recognized
|
||||
- Verify card reader sends keyboard events
|
||||
- Test with the `test_card_reader.py` utility
|
||||
- Check card format is compatible with reader
|
||||
- Ensure card is swiped smoothly at proper speed
|
||||
|
||||
### Card Data Not Captured
|
||||
- Check card data format in logs
|
||||
- Enable debug logging to see raw card data
|
||||
- Test in fallback mode (without evdev) to isolate hardware issues
|
||||
- Verify card swipe completes within 5-second timeout
|
||||
|
||||
### Permission Denied Errors
|
||||
- User may need to be in the `input` group:
|
||||
```bash
|
||||
sudo usermod -a -G input $USER
|
||||
```
|
||||
- Reboot after adding user to group
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for the card reader system:
|
||||
|
||||
1. **Server Validation**: Server validates cards against database and returns authorization
|
||||
2. **Card Enrollment**: Server-side UI for registering new cards
|
||||
3. **Multiple Card Types**: Support for different card formats (barcode, RFID, magnetic)
|
||||
4. **Client-side Validation**: Add optional local card validation before opening edit
|
||||
5. **Audit Trail**: Server tracks all card usage with timestamps
|
||||
6. **RFID Support**: Test and optimize for RFID readers
|
||||
7. **Barcode Scanners**: Support USB barcode scanners as alternative
|
||||
8. **Retry Logic**: Allow re-swipe if card read fails
|
||||
|
||||
## Related Files
|
||||
|
||||
- `/src/main.py` - Main implementation (CardReader class, authentication flow)
|
||||
- `/src/edit_drowing.py` - Drawing/editing interface (uses authenticated user)
|
||||
- `/working_files/test_card_reader.py` - Card reader test utility
|
||||
- `/requirements.txt` - Dependencies (includes evdev)
|
||||
@@ -1,170 +0,0 @@
|
||||
# Card Reader Fix - Multi-USB Device Support
|
||||
|
||||
## Problem Description
|
||||
|
||||
When a USB touchscreen was connected to the Raspberry Pi, the card reader authentication was not working. The system reported "no authentication was received" even though the card reader was physically connected on a different USB port.
|
||||
|
||||
### Root Cause
|
||||
|
||||
The original `find_card_reader()` function used overly broad matching criteria:
|
||||
1. It would select the **first** device with "keyboard" in its name
|
||||
2. USB touchscreens often register as HID keyboard devices (for touch input)
|
||||
3. The touchscreen would be detected first, blocking the actual card reader
|
||||
4. No exclusion logic existed to filter out touch devices
|
||||
|
||||
## Solution
|
||||
|
||||
The fix implements a **priority-based device selection** with **exclusion filters**:
|
||||
|
||||
### 1. Device Exclusion List
|
||||
Devices containing these keywords are now skipped:
|
||||
- `touch`, `touchscreen`
|
||||
- `mouse`, `mice`
|
||||
- `trackpad`, `touchpad`
|
||||
- `pen`, `stylus`
|
||||
- `video`, `button`, `lid`
|
||||
|
||||
### 2. Three-Priority Device Search
|
||||
|
||||
**Priority 1: Explicit Card Readers**
|
||||
- Devices with "card", "reader", "rfid", or "hid" in their name
|
||||
- Must have keyboard capabilities (EV_KEY)
|
||||
- Excludes any device matching exclusion keywords
|
||||
|
||||
**Priority 2: USB Keyboards**
|
||||
- Devices with both "usb" AND "keyboard" in their name
|
||||
- Card readers typically appear as "USB Keyboard" or similar
|
||||
- Excludes touch devices and other non-card peripherals
|
||||
|
||||
**Priority 3: Fallback to Any Keyboard**
|
||||
- Any keyboard device not in the exclusion list
|
||||
- Used only if no card reader or USB keyboard is found
|
||||
|
||||
### 3. Enhanced Logging
|
||||
|
||||
The system now logs:
|
||||
- All detected input devices at startup
|
||||
- Which devices are being skipped and why
|
||||
- Which device is ultimately selected as the card reader
|
||||
|
||||
## Testing
|
||||
|
||||
### Using the Test Script
|
||||
|
||||
Run the enhanced test script to identify your card reader:
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/working_files
|
||||
python3 test_card_reader.py
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. List all input devices with helpful indicators:
|
||||
- `** LIKELY CARD READER **` - devices with "card" or "reader" in name
|
||||
- `(Excluded: ...)` - devices that will be skipped
|
||||
- `(USB Keyboard - could be card reader)` - potential card readers
|
||||
|
||||
2. Auto-detect the card reader using the same logic as the main app
|
||||
|
||||
3. Allow manual selection by device number if auto-detection is wrong
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
=== Available Input Devices ===
|
||||
|
||||
[0] /dev/input/event0
|
||||
Name: USB Touchscreen Controller
|
||||
Phys: usb-0000:01:00.0-1.1/input0
|
||||
Type: Keyboard/HID Input Device
|
||||
(Excluded: appears to be touch/mouse/other non-card device)
|
||||
|
||||
[1] /dev/input/event1
|
||||
Name: HID 08ff:0009
|
||||
Phys: usb-0000:01:00.0-1.2/input0
|
||||
Type: Keyboard/HID Input Device
|
||||
** LIKELY CARD READER **
|
||||
|
||||
[2] /dev/input/event2
|
||||
Name: Logitech USB Keyboard
|
||||
Phys: usb-0000:01:00.0-1.3/input0
|
||||
Type: Keyboard/HID Input Device
|
||||
(USB Keyboard - could be card reader)
|
||||
```
|
||||
|
||||
### Verifying the Fix
|
||||
|
||||
1. **Check Logs**: When the main app starts, check the logs for device detection:
|
||||
```bash
|
||||
tail -f /path/to/logfile
|
||||
```
|
||||
|
||||
Look for messages like:
|
||||
```
|
||||
CardReader: Scanning input devices...
|
||||
CardReader: Skipping excluded device: USB Touchscreen Controller
|
||||
CardReader: Found card reader: HID 08ff:0009 at /dev/input/event1
|
||||
```
|
||||
|
||||
2. **Test Card Swipe**:
|
||||
- Start the signage player
|
||||
- Click the edit button (pencil icon)
|
||||
- Swipe a card
|
||||
- Should successfully authenticate
|
||||
|
||||
3. **Multiple USB Devices**: Test with various USB configurations:
|
||||
- Touchscreen + card reader
|
||||
- Mouse + keyboard + card reader
|
||||
- Multiple USB hubs
|
||||
|
||||
## Configuration
|
||||
|
||||
### If Auto-Detection Fails
|
||||
|
||||
If the automatic detection still selects the wrong device, you can:
|
||||
|
||||
1. **Check device names**: Run `test_card_reader.py` to see all devices
|
||||
2. **Identify your card reader**: Note the exact name of your card reader
|
||||
3. **Add custom exclusions**: If needed, add more keywords to the exclusion list
|
||||
4. **Manual override**: Modify the priority logic to match your specific hardware
|
||||
|
||||
### Permissions
|
||||
|
||||
Ensure the user running the app has permission to access input devices:
|
||||
|
||||
```bash
|
||||
# Add user to input group
|
||||
sudo usermod -a -G input $USER
|
||||
|
||||
# Logout and login again for changes to take effect
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **src/main.py**
|
||||
- Updated `CardReader.find_card_reader()` method
|
||||
- Added exclusion keyword list
|
||||
- Implemented priority-based search
|
||||
- Enhanced logging
|
||||
|
||||
2. **working_files/test_card_reader.py**
|
||||
- Updated `list_input_devices()` to show device classifications
|
||||
- Updated `test_card_reader()` to use same logic as main app
|
||||
- Added visual indicators for device types
|
||||
|
||||
## Compatibility
|
||||
|
||||
This fix is backward compatible:
|
||||
- Works with single-device setups (no touchscreen)
|
||||
- Works with multiple USB devices
|
||||
- Fallback behavior unchanged for systems without card readers
|
||||
- No changes to card data format or server communication
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for specific use cases:
|
||||
|
||||
1. **Configuration file**: Allow specifying device path or name pattern
|
||||
2. **Device caching**: Remember the working device path to avoid re-scanning
|
||||
3. **Hot-plug support**: Detect when card reader is plugged in after app starts
|
||||
4. **Multi-reader support**: Support for multiple card readers simultaneously
|
||||
@@ -1,211 +0,0 @@
|
||||
# Debugging Media File Skips - Guide
|
||||
|
||||
## Summary
|
||||
Your playlist has been analyzed and all 3 media files are present and valid:
|
||||
- ✅ music.jpg (36,481 bytes) - IMAGE - 15s
|
||||
- ✅ 130414-746934884.mp4 (6,474,921 bytes) - VIDEO - 23s
|
||||
- ✅ IMG_0386.jpeg (592,162 bytes) - IMAGE - 15s
|
||||
|
||||
## Enhanced Logging Added
|
||||
The application has been updated with detailed logging to track:
|
||||
- When each media file starts playing
|
||||
- File path validation
|
||||
- File size and existence checks
|
||||
- Media type detection
|
||||
- Widget creation steps
|
||||
- Scheduling of next media
|
||||
- Any errors or skips
|
||||
|
||||
## How to See Detailed Logs
|
||||
|
||||
### Method 1: Run with log output
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage
|
||||
source .venv/bin/activate
|
||||
cd src
|
||||
python3 main.py 2>&1 | tee playback.log
|
||||
```
|
||||
|
||||
### Method 2: Check Kivy logs location
|
||||
Kivy logs are typically stored in:
|
||||
- Linux: `~/.kivy/logs/`
|
||||
- Check with: `ls -lth ~/.kivy/logs/ | head`
|
||||
|
||||
## Common Reasons Media Files Get Skipped
|
||||
|
||||
### 1. **File Not Found**
|
||||
**Symptom**: Log shows "❌ Media file not found"
|
||||
**Cause**: File doesn't exist at expected path
|
||||
**Solution**: Run diagnostic tool
|
||||
```bash
|
||||
python3 diagnose_playlist.py
|
||||
```
|
||||
|
||||
### 2. **Unsupported File Type**
|
||||
**Symptom**: Log shows "❌ Unsupported media type"
|
||||
**Supported formats**:
|
||||
- Videos: .mp4, .avi, .mkv, .mov, .webm
|
||||
- Images: .jpg, .jpeg, .png, .bmp, .gif
|
||||
**Solution**: Convert files or check extension
|
||||
|
||||
### 3. **Video Codec Issues**
|
||||
**Symptom**: Video file exists but doesn't play
|
||||
**Cause**: Video codec not supported by ffpyplayer
|
||||
**Check**: Look for error in logs about codec
|
||||
**Solution**: Re-encode video with H.264 codec:
|
||||
```bash
|
||||
ffmpeg -i input.mp4 -c:v libx264 -preset fast -crf 23 output.mp4
|
||||
```
|
||||
|
||||
### 4. **Corrupted Media Files**
|
||||
**Symptom**: File exists but throws error when loading
|
||||
**Check**: Try playing file with external player
|
||||
```bash
|
||||
# For images
|
||||
feh media/music.jpg
|
||||
|
||||
# For videos
|
||||
vlc media/130414-746934884.mp4
|
||||
# or
|
||||
ffplay media/130414-746934884.mp4
|
||||
```
|
||||
|
||||
### 5. **Memory/Performance Issues**
|
||||
**Symptom**: First few files play, then skipping increases
|
||||
**Cause**: Memory leak or performance degradation
|
||||
**Check**: Look for "consecutive_errors" in logs
|
||||
**Solution**:
|
||||
- Reduce resolution setting in settings popup
|
||||
- Optimize video files (lower bitrate/resolution)
|
||||
|
||||
### 6. **Timing Issues**
|
||||
**Symptom**: Files play too fast or skip immediately
|
||||
**Cause**: Duration set too low or scheduler issues
|
||||
**Check**: Verify durations in playlist.json
|
||||
**Current durations**: 15s (images), 23s (video)
|
||||
|
||||
### 7. **Permission Issues**
|
||||
**Symptom**: "Permission denied" in logs
|
||||
**Check**: File permissions
|
||||
```bash
|
||||
ls -la media/
|
||||
```
|
||||
**Solution**: Fix permissions
|
||||
```bash
|
||||
chmod 644 media/*
|
||||
```
|
||||
|
||||
## What to Look For in Logs
|
||||
|
||||
### Successful Playback Pattern:
|
||||
```
|
||||
SignagePlayer: ===== Playing item 1/3 =====
|
||||
SignagePlayer: File: music.jpg
|
||||
SignagePlayer: Duration: 15s
|
||||
SignagePlayer: Full path: /path/to/media/music.jpg
|
||||
SignagePlayer: ✓ File exists (size: 36,481 bytes)
|
||||
SignagePlayer: Extension: .jpg
|
||||
SignagePlayer: Media type: IMAGE
|
||||
SignagePlayer: Creating AsyncImage widget...
|
||||
SignagePlayer: Adding image widget to content area...
|
||||
SignagePlayer: Scheduled next media in 15s
|
||||
SignagePlayer: ✓ Image displayed successfully
|
||||
SignagePlayer: ✓ Media started successfully (consecutive_errors reset to 0)
|
||||
```
|
||||
|
||||
### Skip Pattern (File Not Found):
|
||||
```
|
||||
SignagePlayer: ===== Playing item 2/3 =====
|
||||
SignagePlayer: File: missing.mp4
|
||||
SignagePlayer: Full path: /path/to/media/missing.mp4
|
||||
SignagePlayer: ❌ Media file not found: /path/to/media/missing.mp4
|
||||
SignagePlayer: Skipping to next media...
|
||||
SignagePlayer: Transitioning to next media (was index 1)
|
||||
```
|
||||
|
||||
### Video Loading Error:
|
||||
```
|
||||
SignagePlayer: Loading video file.mp4 for 23s
|
||||
SignagePlayer: Video provider: ffpyplayer
|
||||
[ERROR ] [Video ] Error reading video
|
||||
[ERROR ] SignagePlayer: Error playing video: ...
|
||||
```
|
||||
|
||||
## Testing Tools Provided
|
||||
|
||||
### 1. Diagnostic Tool
|
||||
```bash
|
||||
python3 diagnose_playlist.py
|
||||
```
|
||||
Checks:
|
||||
- Playlist file exists and is valid
|
||||
- All media files exist
|
||||
- File types are supported
|
||||
- No case sensitivity issues
|
||||
|
||||
### 2. Playback Simulation
|
||||
```bash
|
||||
python3 test_playback_logging.py
|
||||
```
|
||||
Simulates the playback sequence without running the GUI
|
||||
|
||||
## Monitoring Live Playback
|
||||
|
||||
To see live logs while the app is running:
|
||||
```bash
|
||||
# Terminal 1: Start the app
|
||||
./run_player.sh
|
||||
|
||||
# Terminal 2: Monitor logs
|
||||
tail -f ~/.kivy/logs/kivy_*.txt
|
||||
```
|
||||
|
||||
## Quick Fixes to Try
|
||||
|
||||
### 1. Clear any stuck state
|
||||
```bash
|
||||
rm -f src/*.pyc
|
||||
rm -rf src/__pycache__
|
||||
```
|
||||
|
||||
### 2. Test with simpler playlist
|
||||
Create `playlists/test_playlist_v9.json`:
|
||||
```json
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "music.jpg",
|
||||
"url": "media/music.jpg",
|
||||
"duration": 5
|
||||
}
|
||||
],
|
||||
"version": 9
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Check video compatibility
|
||||
```bash
|
||||
# Install ffmpeg tools if not present
|
||||
sudo apt-get install ffmpeg
|
||||
|
||||
# Check video info
|
||||
ffprobe media/130414-746934884.mp4
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
When reporting issues, please provide:
|
||||
1. Output from `python3 diagnose_playlist.py`
|
||||
2. Last 100 lines of Kivy log file
|
||||
3. Any error messages from console
|
||||
4. What you observe (which files skip? pattern?)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Run the app** and observe the console output
|
||||
2. **Check logs** for error patterns
|
||||
3. **Run diagnostic** if files are skipping
|
||||
4. **Test individual files** with external players if needed
|
||||
5. **Re-encode videos** if codec issues found
|
||||
|
||||
The enhanced logging will now tell you exactly why each file is being skipped!
|
||||
@@ -1,263 +0,0 @@
|
||||
# ✅ Kiwy-Signage Authentication Implementation Complete
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
The Kiwy-Signage player now supports **secure authentication** with DigiServer v2 using the flow:
|
||||
|
||||
**hostname → password/quickconnect → get auth_code → use auth_code for API calls**
|
||||
|
||||
## Files Created
|
||||
|
||||
### 1. **Kiwy-Signage/src/player_auth.py**
|
||||
- Complete authentication module
|
||||
- Handles authentication, token storage, API calls
|
||||
- Methods:
|
||||
- `authenticate()` - Initial authentication with server
|
||||
- `verify_auth()` - Verify saved auth code
|
||||
- `get_playlist()` - Fetch playlist using auth code
|
||||
- `send_heartbeat()` - Send status updates
|
||||
- `send_feedback()` - Send player feedback
|
||||
- `clear_auth()` - Clear saved credentials
|
||||
|
||||
### 2. **Kiwy-Signage/src/get_playlists_v2.py**
|
||||
- Updated playlist management
|
||||
- Uses new authentication system
|
||||
- Backward compatible with existing code
|
||||
- Functions:
|
||||
- `ensure_authenticated()` - Auto-authenticate if needed
|
||||
- `fetch_server_playlist()` - Get playlist via authenticated API
|
||||
- `send_player_feedback()` - Send feedback with auth
|
||||
- All existing functions updated to use auth
|
||||
|
||||
### 3. **Kiwy-Signage/test_authentication.py**
|
||||
- Test script to verify authentication
|
||||
- Run before updating main.py
|
||||
- Tests:
|
||||
- Server connectivity
|
||||
- Authentication flow
|
||||
- Playlist fetch
|
||||
- Heartbeat sending
|
||||
|
||||
### 4. **Kiwy-Signage/MIGRATION_GUIDE.md**
|
||||
- Complete migration instructions
|
||||
- Troubleshooting guide
|
||||
- Configuration examples
|
||||
- Rollback procedures
|
||||
|
||||
### 5. **digiserver-v2/player_auth_module.py**
|
||||
- Standalone authentication module
|
||||
- Can be used in any Python project
|
||||
- Same functionality as Kiwy-Signage version
|
||||
|
||||
### 6. **digiserver-v2/PLAYER_AUTH.md**
|
||||
- Complete API documentation
|
||||
- Authentication endpoint specs
|
||||
- Configuration file formats
|
||||
- Security considerations
|
||||
|
||||
## Testing Steps
|
||||
|
||||
### 1. Test Authentication (Recommended First)
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage
|
||||
python3 test_authentication.py
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
✅ Server is healthy (version: 2.0.0)
|
||||
🔐 Authenticating with server...
|
||||
✅ Authentication successful!
|
||||
Player: Demo Player
|
||||
📋 Testing playlist fetch...
|
||||
✅ Playlist received!
|
||||
💓 Testing heartbeat...
|
||||
✅ Heartbeat sent successfully
|
||||
✅ All tests passed! Player is ready to use.
|
||||
```
|
||||
|
||||
### 2. Update Main Player App
|
||||
|
||||
In `Kiwy-Signage/src/main.py`, change:
|
||||
|
||||
```python
|
||||
# OLD:
|
||||
from get_playlists import update_playlist_if_needed, ...
|
||||
|
||||
# NEW:
|
||||
from get_playlists_v2 import update_playlist_if_needed, ...
|
||||
```
|
||||
|
||||
### 3. Run Player
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/src
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### No Changes Needed!
|
||||
|
||||
Your existing `app_config.txt` works as-is:
|
||||
|
||||
```json
|
||||
{
|
||||
"server_ip": "192.168.1.100",
|
||||
"port": "5000",
|
||||
"screen_name": "player-001",
|
||||
"quickconnect_key": "QUICK123",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication Storage
|
||||
|
||||
Auto-created at `src/player_auth.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hostname": "player-001",
|
||||
"auth_code": "rrX4JtM99e4e6ni0VCsuIstjTVQQqILXeRmGu_Ek2Ks",
|
||||
"player_id": 1,
|
||||
"player_name": "Demo Player",
|
||||
"group_id": 5,
|
||||
"orientation": "Landscape",
|
||||
"authenticated": true,
|
||||
"server_url": "http://192.168.1.100:5000"
|
||||
}
|
||||
```
|
||||
|
||||
## DigiServer v2 Setup
|
||||
|
||||
### 1. Create Player
|
||||
|
||||
Via Web UI (http://your-server:5000):
|
||||
1. Login as admin
|
||||
2. Go to Players → Add Player
|
||||
3. Fill in:
|
||||
- **Name**: Display name
|
||||
- **Hostname**: player-001 (must match `screen_name` in config)
|
||||
- **Password**: (optional, use quickconnect instead)
|
||||
- **Quick Connect Code**: QUICK123 (must match `quickconnect_key`)
|
||||
- **Orientation**: Landscape/Portrait
|
||||
|
||||
### 2. Test API Manually
|
||||
|
||||
```bash
|
||||
# Test authentication
|
||||
curl -X POST http://your-server:5000/api/auth/player \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"hostname": "player-001",
|
||||
"quickconnect_code": "QUICK123"
|
||||
}'
|
||||
|
||||
# Expected response:
|
||||
{
|
||||
"success": true,
|
||||
"player_id": 1,
|
||||
"player_name": "Demo Player",
|
||||
"auth_code": "rrX4JtM99e4e6ni0VCsuIstjTVQQqILXeRmGu_Ek2Ks",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
✅ **Auth Code Storage**: Saved locally, not transmitted after initial auth
|
||||
✅ **Bcrypt Hashing**: Passwords and quickconnect codes hashed in database
|
||||
✅ **Token-Based**: Auth codes are 32-byte URL-safe tokens
|
||||
✅ **Rate Limiting**: Authentication endpoint limited to 10 requests/minute
|
||||
✅ **Session Management**: Server tracks player sessions and status
|
||||
|
||||
## Advantages Over Old System
|
||||
|
||||
### Old System (v1)
|
||||
```
|
||||
Player → [hostname + quickconnect on EVERY request] → Server
|
||||
↓
|
||||
Bcrypt verification on every API call (slow)
|
||||
```
|
||||
|
||||
### New System (v2)
|
||||
```
|
||||
Player → [hostname + quickconnect ONCE] → Server
|
||||
↓
|
||||
Returns auth_code
|
||||
↓
|
||||
Player → [auth_code for all subsequent requests] → Server
|
||||
↓
|
||||
Fast token validation
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- 🚀 **10x faster API calls** (no bcrypt on every request)
|
||||
- 🔒 **More secure** (credentials only sent once)
|
||||
- 📊 **Better tracking** (server knows player sessions)
|
||||
- 🔄 **Easier management** (can revoke auth codes)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Fails
|
||||
|
||||
**Check:**
|
||||
1. Player exists in DigiServer v2 with matching hostname
|
||||
2. Quickconnect code matches exactly (case-sensitive)
|
||||
3. Server is accessible: `curl http://server:5000/api/health`
|
||||
|
||||
### Auth Code Expired
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
rm /home/pi/Desktop/Kiwy-Signage/src/player_auth.json
|
||||
# Restart player - will auto-authenticate
|
||||
```
|
||||
|
||||
### Old get_playlists.py Issues
|
||||
|
||||
**Keep both files:**
|
||||
- `get_playlists.py` - Original (for DigiServer v1)
|
||||
- `get_playlists_v2.py` - New (for DigiServer v2)
|
||||
|
||||
Can switch between them by changing import in `main.py`.
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ **Test authentication** with `test_authentication.py`
|
||||
2. ✅ **Update main.py** to use `get_playlists_v2`
|
||||
3. ✅ **Run player** and verify playlist loading
|
||||
4. ✅ **Monitor logs** for first 24 hours
|
||||
5. ✅ **Update other players** one at a time
|
||||
|
||||
## Files Summary
|
||||
|
||||
```
|
||||
Kiwy-Signage/
|
||||
├── src/
|
||||
│ ├── player_auth.py # ✨ NEW: Authentication module
|
||||
│ ├── get_playlists_v2.py # ✨ NEW: Updated playlist fetcher
|
||||
│ ├── get_playlists.py # OLD: Keep for v1 compatibility
|
||||
│ ├── main.py # Update import to use v2
|
||||
│ └── player_auth.json # ✨ AUTO-CREATED: Auth storage
|
||||
├── test_authentication.py # ✨ NEW: Test script
|
||||
├── MIGRATION_GUIDE.md # ✨ NEW: Migration docs
|
||||
└── resources/
|
||||
└── app_config.txt # Existing config (no changes needed)
|
||||
|
||||
digiserver-v2/
|
||||
├── app/
|
||||
│ ├── models/player.py # ✨ UPDATED: Added auth methods
|
||||
│ └── blueprints/api.py # ✨ UPDATED: Added auth endpoints
|
||||
├── player_auth_module.py # ✨ NEW: Standalone module
|
||||
├── player_config_template.ini # ✨ NEW: Config template
|
||||
├── PLAYER_AUTH.md # ✨ NEW: API documentation
|
||||
└── reinit_db.sh # ✨ NEW: Database recreation script
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Implementation Complete!
|
||||
|
||||
The Kiwy-Signage player authentication system is now compatible with DigiServer v2 using secure token-based authentication. Test with `test_authentication.py` before deploying to production.
|
||||
@@ -1,182 +0,0 @@
|
||||
# Investigation Results: Media File Skipping
|
||||
|
||||
## Diagnostic Summary
|
||||
✅ **All 3 media files are present and valid:**
|
||||
- music.jpg (36,481 bytes) - IMAGE
|
||||
- 130414-746934884.mp4 (6,474,921 bytes) - VIDEO (H.264, 1920x1080, compatible)
|
||||
- IMG_0386.jpeg (592,162 bytes) - IMAGE
|
||||
|
||||
✅ **No file system issues found:**
|
||||
- All files exist
|
||||
- Correct permissions
|
||||
- No case sensitivity problems
|
||||
- Supported file types
|
||||
|
||||
✅ **Video codec is compatible:**
|
||||
- H.264 codec (fully supported by ffpyplayer)
|
||||
- 1920x1080 @ 29.97fps
|
||||
- Reasonable bitrate (2.3 Mbps)
|
||||
|
||||
## Potential Root Causes Identified
|
||||
|
||||
### 1. **Video Widget Not Properly Stopping** (Most Likely)
|
||||
When transitioning from video to the next media, the video widget may not be properly stopped before removal. This could cause:
|
||||
- The video to continue playing in background
|
||||
- Race conditions with scheduling
|
||||
- Next media appearing to "skip"
|
||||
|
||||
**Location**: `play_current_media()` line 417-420
|
||||
```python
|
||||
if self.current_widget:
|
||||
self.ids.content_area.remove_widget(self.current_widget)
|
||||
self.current_widget = None
|
||||
```
|
||||
|
||||
**Fix**: Stop video before removing widget
|
||||
|
||||
### 2. **Multiple Scheduled Events**
|
||||
The `Clock.schedule_once(self.next_media, duration)` could be called multiple times if widget loading triggers multiple events.
|
||||
|
||||
**Location**: Lines 510, 548
|
||||
|
||||
**Fix**: Add `Clock.unschedule()` before scheduling
|
||||
|
||||
### 3. **Video Loading Callback Issues**
|
||||
The video `loaded` callback might not fire or might fire multiple times, causing state confusion.
|
||||
|
||||
**Location**: `_on_video_loaded()` line 516
|
||||
|
||||
### 4. **Pause State Not Properly Checked**
|
||||
If the player gets paused/unpaused during media transition, scheduling could get confused.
|
||||
|
||||
**Location**: `next_media()` line 551
|
||||
|
||||
## What Enhanced Logging Will Show
|
||||
|
||||
With the new logging, you'll see patterns like:
|
||||
|
||||
### If Videos Are Being Skipped:
|
||||
```
|
||||
===== Playing item 2/3 =====
|
||||
File: 130414-746934884.mp4
|
||||
Extension: .mp4
|
||||
Media type: VIDEO
|
||||
Loading video...
|
||||
Creating Video widget...
|
||||
[SHORT PAUSE OR ERROR]
|
||||
Transitioning to next media (was index 1)
|
||||
===== Playing item 3/3 =====
|
||||
```
|
||||
|
||||
### If Duration Is Too Short:
|
||||
```
|
||||
Creating Video widget...
|
||||
Scheduled next media in 23s
|
||||
[Only 1-2 seconds pass]
|
||||
Transitioning to next media
|
||||
```
|
||||
|
||||
## Recommended Fixes
|
||||
|
||||
I've added comprehensive logging. Here are additional fixes to try:
|
||||
|
||||
### Fix 1: Properly Stop Video Widget Before Removal
|
||||
Add this to `play_current_media()` before removing widget:
|
||||
|
||||
```python
|
||||
# Remove previous media widget
|
||||
if self.current_widget:
|
||||
# Stop video if it's playing
|
||||
if isinstance(self.current_widget, Video):
|
||||
self.current_widget.state = 'stop'
|
||||
self.current_widget.unload()
|
||||
self.ids.content_area.remove_widget(self.current_widget)
|
||||
self.current_widget = None
|
||||
```
|
||||
|
||||
### Fix 2: Ensure Scheduled Events Don't Overlap
|
||||
Modify scheduling in both `play_video()` and `play_image()`:
|
||||
|
||||
```python
|
||||
# Unschedule any pending transitions before scheduling new one
|
||||
Clock.unschedule(self.next_media)
|
||||
Clock.schedule_once(self.next_media, duration)
|
||||
```
|
||||
|
||||
### Fix 3: Add Video State Monitoring
|
||||
Track when video actually starts playing vs when widget is created.
|
||||
|
||||
## How to Test
|
||||
|
||||
### 1. Run with Enhanced Logging
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage
|
||||
source .venv/bin/activate
|
||||
cd src
|
||||
python3 main.py 2>&1 | tee ../playback_debug.log
|
||||
```
|
||||
|
||||
Watch the console output. You should see:
|
||||
- Each media file being loaded
|
||||
- Timing information
|
||||
- Any errors or skips
|
||||
|
||||
### 2. Check Timing
|
||||
If media skips, check the log for timing:
|
||||
- Does "Scheduled next media in Xs" appear?
|
||||
- How long until "Transitioning to next media" appears?
|
||||
- Is it immediate (< 1 second) = scheduling bug
|
||||
- Is it after full duration = normal operation
|
||||
|
||||
### 3. Look for Error Patterns
|
||||
Search the log for:
|
||||
```bash
|
||||
grep "❌" playback_debug.log
|
||||
grep "Error" playback_debug.log
|
||||
grep "consecutive_errors" playback_debug.log
|
||||
```
|
||||
|
||||
## Quick Test Scenario
|
||||
|
||||
Create a test with just one file to isolate the issue:
|
||||
|
||||
```json
|
||||
{
|
||||
"playlist": [
|
||||
{
|
||||
"file_name": "music.jpg",
|
||||
"url": "media/music.jpg",
|
||||
"duration": 10
|
||||
}
|
||||
],
|
||||
"version": 99
|
||||
}
|
||||
```
|
||||
|
||||
If this single image repeats correctly every 10s, the issue is with video playback or transitions.
|
||||
|
||||
## What to Report
|
||||
|
||||
When you run the app, please capture:
|
||||
|
||||
1. **Console output** - especially the pattern around skipped files
|
||||
2. **Which files skip?** - Is it always videos? Always after videos?
|
||||
3. **Timing** - Do files play for full duration before skipping?
|
||||
4. **Pattern** - First loop OK then skips? Always skips certain file?
|
||||
|
||||
## Tools Created
|
||||
|
||||
1. **diagnose_playlist.py** - Check file system issues
|
||||
2. **test_playback_logging.py** - Simulate playback logic
|
||||
3. **check_video_codecs.py** - Verify video compatibility
|
||||
4. **Enhanced main.py** - Detailed logging throughout
|
||||
|
||||
## Next Actions
|
||||
|
||||
1. ✅ Run `diagnose_playlist.py` - **PASSED**
|
||||
2. ✅ Run `check_video_codecs.py` - **PASSED**
|
||||
3. ⏳ Run app with logging and observe pattern
|
||||
4. ⏳ Apply video widget fixes if needed
|
||||
5. ⏳ Report findings for further diagnosis
|
||||
|
||||
The enhanced logging will pinpoint exactly where and why files are being skipped!
|
||||
@@ -1,276 +0,0 @@
|
||||
# Kiwy-Signage Player Migration Guide
|
||||
## Updating to DigiServer v2 Authentication
|
||||
|
||||
This guide explains how to update your Kiwy-Signage player to use the new secure authentication system with DigiServer v2.
|
||||
|
||||
## What Changed?
|
||||
|
||||
### Old System (v1)
|
||||
- Direct API calls with hostname + quickconnect code on every request
|
||||
- No persistent authentication
|
||||
- Credentials sent with every API call
|
||||
|
||||
### New System (v2)
|
||||
- **Step 1**: Authenticate once with hostname + password/quickconnect
|
||||
- **Step 2**: Receive and save auth_code
|
||||
- **Step 3**: Use auth_code for all subsequent API calls
|
||||
- **Benefits**: More secure, faster, supports session management
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### 1. Copy New Files
|
||||
|
||||
Copy the authentication modules to your Kiwy-Signage project:
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/src/
|
||||
|
||||
# New files are already created:
|
||||
# - player_auth.py (authentication module)
|
||||
# - get_playlists_v2.py (updated playlist fetcher)
|
||||
```
|
||||
|
||||
### 2. Update main.py Imports
|
||||
|
||||
In `main.py`, replace the old import:
|
||||
|
||||
```python
|
||||
# OLD:
|
||||
from get_playlists import (
|
||||
update_playlist_if_needed,
|
||||
send_playing_status_feedback,
|
||||
send_playlist_restart_feedback,
|
||||
send_player_error_feedback
|
||||
)
|
||||
|
||||
# NEW:
|
||||
from get_playlists_v2 import (
|
||||
update_playlist_if_needed,
|
||||
send_playing_status_feedback,
|
||||
send_playlist_restart_feedback,
|
||||
send_player_error_feedback
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Add Authentication on Startup
|
||||
|
||||
In `main.py`, add authentication check in the `SignagePlayer` class:
|
||||
|
||||
```python
|
||||
def build(self):
|
||||
"""Build the application UI"""
|
||||
# Load configuration
|
||||
self.config = self.load_config()
|
||||
|
||||
# NEW: Authenticate with server
|
||||
from player_auth import PlayerAuth
|
||||
auth = PlayerAuth()
|
||||
|
||||
if not auth.is_authenticated():
|
||||
Logger.info("First time setup - authenticating...")
|
||||
from get_playlists_v2 import ensure_authenticated
|
||||
if not ensure_authenticated(self.config):
|
||||
Logger.error("❌ Failed to authenticate with server!")
|
||||
# Show error popup or retry
|
||||
else:
|
||||
Logger.info(f"✅ Authenticated as: {auth.get_player_name()}")
|
||||
|
||||
# Continue with normal startup...
|
||||
return SignagePlayerWidget(config=self.config)
|
||||
```
|
||||
|
||||
### 4. Update Server Configuration
|
||||
|
||||
Your existing `app_config.txt` works as-is! The new system uses the same fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"server_ip": "your-server-ip",
|
||||
"port": "5000",
|
||||
"screen_name": "player-001",
|
||||
"quickconnect_key": "QUICK123",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: `screen_name` is now used as `hostname` for authentication.
|
||||
|
||||
### 5. Testing
|
||||
|
||||
1. **Stop the old player**:
|
||||
```bash
|
||||
pkill -f main.py
|
||||
```
|
||||
|
||||
2. **Delete old authentication data** (first time only):
|
||||
```bash
|
||||
rm -f /home/pi/Desktop/Kiwy-Signage/src/player_auth.json
|
||||
```
|
||||
|
||||
3. **Start the updated player**:
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/src/
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
4. **Check logs for authentication**:
|
||||
- Look for: `✅ Authentication successful`
|
||||
- Or: `❌ Authentication failed: [error message]`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Fails
|
||||
|
||||
**Problem**: `❌ Authentication failed: Invalid credentials`
|
||||
|
||||
**Solution**:
|
||||
1. Verify player exists in DigiServer v2:
|
||||
- Login to http://your-server:5000
|
||||
- Go to Players → check if hostname exists
|
||||
|
||||
2. Verify quickconnect code:
|
||||
- In DigiServer, check player's Quick Connect Code
|
||||
- Update `app_config.txt` with correct code
|
||||
|
||||
3. Check server URL:
|
||||
```python
|
||||
# Test connection
|
||||
import requests
|
||||
response = requests.get('http://your-server:5000/api/health')
|
||||
print(response.json()) # Should show: {'status': 'healthy'}
|
||||
```
|
||||
|
||||
### Auth Code Expired
|
||||
|
||||
**Problem**: Player was working, now shows auth errors
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Clear saved auth and re-authenticate
|
||||
rm /home/pi/Desktop/Kiwy-Signage/src/player_auth.json
|
||||
# Restart player - will auto-authenticate
|
||||
```
|
||||
|
||||
### Can't Connect to Server
|
||||
|
||||
**Problem**: `Cannot connect to server`
|
||||
|
||||
**Solution**:
|
||||
1. Check server is running:
|
||||
```bash
|
||||
curl http://your-server:5000/api/health
|
||||
```
|
||||
|
||||
2. Check network connectivity:
|
||||
```bash
|
||||
ping your-server-ip
|
||||
```
|
||||
|
||||
3. Verify server URL in `app_config.txt`
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### player_auth.json (auto-created)
|
||||
|
||||
This file stores the authentication token:
|
||||
|
||||
```json
|
||||
{
|
||||
"hostname": "player-001",
|
||||
"auth_code": "rrX4JtM99e4e6ni0VCsuIstjTVQQqILXeRmGu_Ek2Ks",
|
||||
"player_id": 1,
|
||||
"player_name": "Demo Player",
|
||||
"group_id": 5,
|
||||
"orientation": "Landscape",
|
||||
"authenticated": true,
|
||||
"server_url": "http://your-server:5000"
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: Keep this file secure! It contains your player's access token.
|
||||
|
||||
## Advanced: Custom Authentication
|
||||
|
||||
If you need custom authentication logic:
|
||||
|
||||
```python
|
||||
from player_auth import PlayerAuth
|
||||
|
||||
# Initialize
|
||||
auth = PlayerAuth(config_file='custom_auth.json')
|
||||
|
||||
# Authenticate with password instead of quickconnect
|
||||
success, error = auth.authenticate(
|
||||
server_url='http://your-server:5000',
|
||||
hostname='player-001',
|
||||
password='your_secure_password' # Use password instead
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f"✅ Authenticated as: {auth.get_player_name()}")
|
||||
|
||||
# Get playlist
|
||||
playlist_data = auth.get_playlist()
|
||||
|
||||
# Send heartbeat
|
||||
auth.send_heartbeat(status='playing')
|
||||
|
||||
# Send feedback
|
||||
auth.send_feedback(
|
||||
message="Playing video.mp4",
|
||||
status="playing",
|
||||
playlist_version=5
|
||||
)
|
||||
else:
|
||||
print(f"❌ Failed: {error}")
|
||||
```
|
||||
|
||||
## Rollback to Old System
|
||||
|
||||
If you need to rollback:
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/src/
|
||||
|
||||
# Rename new files
|
||||
mv get_playlists_v2.py get_playlists_v2.py.backup
|
||||
mv player_auth.py player_auth.py.backup
|
||||
|
||||
# Use old get_playlists.py (keep as-is)
|
||||
# Old system will continue working with DigiServer v1
|
||||
```
|
||||
|
||||
## Benefits of New System
|
||||
|
||||
✅ **More Secure**: Auth tokens instead of passwords in every request
|
||||
✅ **Better Performance**: No bcrypt verification on every API call
|
||||
✅ **Session Management**: Server tracks player sessions
|
||||
✅ **Easier Debugging**: Auth failures vs API failures are separate
|
||||
✅ **Future-Proof**: Ready for token refresh, expiration, etc.
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once migration is complete:
|
||||
|
||||
1. **Monitor player logs** for first 24 hours
|
||||
2. **Verify playlist updates** are working
|
||||
3. **Check feedback** is being received in DigiServer
|
||||
4. **Update other players** one at a time
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. **Check player logs**: `tail -f player.log`
|
||||
2. **Check server logs**: DigiServer v2 logs in `instance/logs/`
|
||||
3. **Test API manually**:
|
||||
```bash
|
||||
# Test authentication
|
||||
curl -X POST http://your-server:5000/api/auth/player \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"hostname":"player-001","quickconnect_code":"QUICK123"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Migration completed!** Your Kiwy-Signage player now uses secure authentication with DigiServer v2. 🎉
|
||||
@@ -1,43 +0,0 @@
|
||||
INFO ] [Kivy ] Installed at "/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/kivy/__init__.py"
|
||||
[INFO ] [Python ] v3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0]
|
||||
[INFO ] [Python ] Interpreter at "/home/pi/Desktop/Kiwy-Signage/.venv/bin/python3"
|
||||
[INFO ] [Logger ] Purge log fired. Processing...
|
||||
[INFO ] [Logger ] Purge finished!
|
||||
[DEBUG ] [Using selector] EpollSelector
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
|
||||
[ERROR ] [Image ] Error loading </home/pi/Desktop/Kiwy-Signage/config/resources/intro1.mp4>
|
||||
[WARNING] ⚠️ SSL verification disabled - NOT recommended for production!
|
||||
[DEBUG ] [Starting new HTTPS connection (1)] 192.168.0.121:443
|
||||
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
||||
warnings.warn(
|
||||
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/auth/verify HTTP/1.1" 200 None
|
||||
[INFO ] ✅ Auth code verified
|
||||
[INFO ] ✅ Using existing authentication
|
||||
[INFO ] [Fetching playlist from] https://192.168.0.121:443/api/playlists/1
|
||||
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
||||
warnings.warn(
|
||||
[DEBUG ] [https ]//192.168.0.121:443 "GET /api/playlists/1 HTTP/1.1" 200 None
|
||||
[INFO ] [✅ Playlist received (version] 34)
|
||||
[INFO ] [📊 Playlist versions - Server] v34, Local: v34
|
||||
[INFO ] ✓ Playlist is up to date
|
||||
[WARNING] Deprecated property "<BooleanProperty name=allow_stretch>" of object "<kivy.uix.image.AsyncImage object at 0x7fa5f79ef0>" has been set, it will be removed in a future version
|
||||
[WARNING] Deprecated property "<BooleanProperty name=keep_ratio>" of object "<kivy.uix.image.AsyncImage object at 0x7fa5f79ef0>" was accessed, it will be removed in a future version
|
||||
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
||||
warnings.warn(
|
||||
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/auth/verify HTTP/1.1" 200 None
|
||||
[INFO ] ✅ Auth code verified
|
||||
[INFO ] ✅ Using existing authentication
|
||||
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
||||
warnings.warn(
|
||||
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/player-feedback HTTP/1.1" 200 None
|
||||
^C[2026-01-17 22:09:12] 🛑 Watchdog received stop signal
|
||||
pi@rpi-tvcanba1:~/Desktop/Kiwy-Signage $
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
# Offline Installation Guide
|
||||
|
||||
This guide explains how to set up and use offline installation for the Kiwy Signage Player.
|
||||
|
||||
## Overview
|
||||
|
||||
The offline installation system allows you to install the signage player on devices without internet access by pre-downloading all necessary packages.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
Kiwy-Signage/
|
||||
├── repo/ # Offline packages repository
|
||||
│ ├── python-wheels/ # Python packages (.whl files)
|
||||
│ ├── system-packages/ # System package information
|
||||
│ │ ├── apt-packages.txt # List of required apt packages
|
||||
│ │ └── debs/ # Downloaded .deb files (optional)
|
||||
│ └── README.md
|
||||
├── download_offline_packages.sh # Download Python packages
|
||||
├── download_deb_packages.sh # Download system .deb packages
|
||||
└── install.sh # Smart installer (online/offline)
|
||||
```
|
||||
|
||||
## Setup for Offline Installation
|
||||
|
||||
### Step 1: Prepare on a Connected System
|
||||
|
||||
On a system with internet access (preferably Raspberry Pi OS):
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd Kiwy-Signage
|
||||
|
||||
# Download Python packages
|
||||
bash download_offline_packages.sh
|
||||
|
||||
# (Optional) Download system .deb packages
|
||||
bash download_deb_packages.sh
|
||||
```
|
||||
|
||||
This will populate the `repo/` folder with all necessary packages.
|
||||
|
||||
### Step 2: Transfer to Offline System
|
||||
|
||||
Copy the entire `Kiwy-Signage` directory to your offline system:
|
||||
|
||||
```bash
|
||||
# Using USB drive
|
||||
cp -r Kiwy-Signage /media/usb/
|
||||
|
||||
# Or create a tarball
|
||||
tar -czf kiwy-signage-offline.tar.gz Kiwy-Signage/
|
||||
|
||||
# On target system, extract:
|
||||
tar -xzf kiwy-signage-offline.tar.gz
|
||||
cd Kiwy-Signage
|
||||
```
|
||||
|
||||
### Step 3: Install on Offline System
|
||||
|
||||
The installer automatically detects offline packages:
|
||||
|
||||
```bash
|
||||
# Automatic detection
|
||||
bash install.sh
|
||||
|
||||
# Or explicitly specify offline mode
|
||||
bash install.sh --offline
|
||||
```
|
||||
|
||||
## Package Information
|
||||
|
||||
### Python Packages (requirements.txt)
|
||||
|
||||
- **kivy==2.1.0** - UI framework
|
||||
- **requests==2.32.4** - HTTP library
|
||||
- **bcrypt==4.2.1** - Password hashing
|
||||
- **aiohttp==3.9.1** - Async HTTP client
|
||||
- **asyncio==3.4.3** - Async I/O framework
|
||||
|
||||
### System Packages (APT)
|
||||
|
||||
See `repo/system-packages/apt-packages.txt` for complete list:
|
||||
- Python development tools
|
||||
- SDL2 libraries (video/audio)
|
||||
- FFmpeg and codecs
|
||||
- GStreamer plugins
|
||||
- Build dependencies
|
||||
|
||||
## Online Installation
|
||||
|
||||
If you have internet access, simply run:
|
||||
|
||||
```bash
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
The installer will automatically download and install all packages from the internet.
|
||||
|
||||
## Updating Offline Packages
|
||||
|
||||
To update the offline package cache:
|
||||
|
||||
```bash
|
||||
# On a connected system
|
||||
bash download_offline_packages.sh
|
||||
```
|
||||
|
||||
This will download the latest versions of all packages.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Missing Dependencies
|
||||
|
||||
If installation fails due to missing dependencies:
|
||||
|
||||
```bash
|
||||
# Download .deb packages with dependencies
|
||||
bash download_deb_packages.sh
|
||||
|
||||
# Install with dependency resolution
|
||||
sudo apt install -f
|
||||
```
|
||||
|
||||
### Problem: Wheel Not Found
|
||||
|
||||
If a specific Python package wheel is not found:
|
||||
|
||||
```bash
|
||||
# Download specific package
|
||||
pip3 download <package-name> -d repo/python-wheels/
|
||||
```
|
||||
|
||||
### Problem: Architecture Mismatch
|
||||
|
||||
Ensure packages are downloaded on the same architecture (ARM for Raspberry Pi):
|
||||
|
||||
```bash
|
||||
# Verify architecture
|
||||
uname -m # Should show: armv7l or aarch64
|
||||
|
||||
# Force ARM downloads
|
||||
pip3 download -r requirements.txt -d repo/python-wheels/ --platform linux_armv7l
|
||||
```
|
||||
|
||||
## Storage Requirements
|
||||
|
||||
- **Python wheels**: ~50-100 MB
|
||||
- **System .deb packages**: ~200-500 MB (if downloaded)
|
||||
- **Total**: ~250-600 MB
|
||||
|
||||
## Notes
|
||||
|
||||
- The `repo/` folder is designed to be portable
|
||||
- Downloaded packages are excluded from git (see `.gitignore`)
|
||||
- The installer supports both online and offline modes seamlessly
|
||||
- System packages list is maintained in `repo/system-packages/apt-packages.txt`
|
||||
@@ -1,42 +0,0 @@
|
||||
# Offline Installation Quick Start
|
||||
|
||||
## For Connected System (Preparation)
|
||||
|
||||
```bash
|
||||
# 1. Download Python packages (required)
|
||||
bash download_offline_packages.sh
|
||||
|
||||
# 2. Download system .deb packages (optional, for fully offline)
|
||||
bash download_deb_packages.sh
|
||||
```
|
||||
|
||||
## For Offline System (Installation)
|
||||
|
||||
```bash
|
||||
# The installer auto-detects offline packages
|
||||
bash install.sh
|
||||
|
||||
# Or explicitly use offline mode
|
||||
bash install.sh --offline
|
||||
```
|
||||
|
||||
## What's Included
|
||||
|
||||
### Python Packages (18 wheels)
|
||||
✅ Kivy 2.1.0
|
||||
✅ Requests 2.32.4
|
||||
✅ Bcrypt 4.2.1
|
||||
✅ Aiohttp 3.9.1 (async HTTP)
|
||||
✅ Asyncio 3.4.3 (async framework)
|
||||
✅ All dependencies
|
||||
|
||||
### System Packages
|
||||
📋 See `repo/system-packages/apt-packages.txt`
|
||||
|
||||
## File Size
|
||||
- Python wheels: ~50 MB
|
||||
- System packages: ~200-500 MB (if .deb downloaded)
|
||||
|
||||
## See Also
|
||||
- **OFFLINE_INSTALLATION.md** - Complete guide
|
||||
- **repo/README.md** - Repository structure
|
||||
@@ -1,190 +0,0 @@
|
||||
# 🚀 Quick Start Guide - Player Authentication
|
||||
|
||||
## For DigiServer Admin
|
||||
|
||||
### 1. Create Player in DigiServer v2
|
||||
|
||||
```bash
|
||||
# Login to web interface
|
||||
http://your-server:5000
|
||||
|
||||
# Navigate to: Players → Add Player
|
||||
Name: Office Player
|
||||
Hostname: office-player-001 # Must be unique
|
||||
Location: Main Office
|
||||
Password: [leave empty if using quickconnect]
|
||||
Quick Connect Code: OFFICE123 # Easy pairing code
|
||||
Orientation: Landscape
|
||||
```
|
||||
|
||||
### 2. Distribute Credentials to Player
|
||||
|
||||
Give the player administrator:
|
||||
- **Server URL**: `http://your-server:5000`
|
||||
- **Hostname**: `office-player-001`
|
||||
- **Quick Connect Code**: `OFFICE123`
|
||||
|
||||
## For Player Setup
|
||||
|
||||
### 1. Update app_config.txt
|
||||
|
||||
```json
|
||||
{
|
||||
"server_ip": "your-server-ip",
|
||||
"port": "5000",
|
||||
"screen_name": "office-player-001",
|
||||
"quickconnect_key": "OFFICE123",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Test Authentication
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage
|
||||
python3 test_authentication.py
|
||||
```
|
||||
|
||||
### 3. Update Player Code (One-Time)
|
||||
|
||||
In `src/main.py`, line ~34, change:
|
||||
|
||||
```python
|
||||
from get_playlists_v2 import ( # Changed from get_playlists
|
||||
update_playlist_if_needed,
|
||||
send_playing_status_feedback,
|
||||
send_playlist_restart_feedback,
|
||||
send_player_error_feedback
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Run Player
|
||||
|
||||
```bash
|
||||
cd /home/pi/Desktop/Kiwy-Signage/src
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
```
|
||||
┌─────────┐ ┌────────────┐
|
||||
│ Player │ │ DigiServer │
|
||||
└────┬────┘ └─────┬──────┘
|
||||
│ │
|
||||
│ POST /api/auth/player │
|
||||
│ {hostname, quickconnect} │
|
||||
├──────────────────────────────>│
|
||||
│ │
|
||||
│ 200 OK │
|
||||
│ {auth_code, player_id, ...} │
|
||||
│<──────────────────────────────┤
|
||||
│ │
|
||||
│ Save auth_code locally │
|
||||
├──────────────────┐ │
|
||||
│ │ │
|
||||
│<─────────────────┘ │
|
||||
│ │
|
||||
│ GET /api/playlists/{id} │
|
||||
│ Header: Bearer {auth_code} │
|
||||
├──────────────────────────────>│
|
||||
│ │
|
||||
│ 200 OK │
|
||||
│ {playlist, version} │
|
||||
│<──────────────────────────────┤
|
||||
│ │
|
||||
```
|
||||
|
||||
## Files to Know
|
||||
|
||||
### Player Side (Kiwy-Signage)
|
||||
|
||||
```
|
||||
src/
|
||||
├── player_auth.json # Auto-created, stores auth_code
|
||||
├── player_auth.py # Authentication module
|
||||
├── get_playlists_v2.py # Updated playlist fetcher
|
||||
└── app_config.txt # Your existing config
|
||||
```
|
||||
|
||||
### Server Side (DigiServer v2)
|
||||
|
||||
```
|
||||
app/
|
||||
├── models/player.py # Player model with auth methods
|
||||
└── blueprints/api.py # Authentication endpoints
|
||||
|
||||
API Endpoints:
|
||||
- POST /api/auth/player # Authenticate and get token
|
||||
- POST /api/auth/verify # Verify token validity
|
||||
- GET /api/playlists/{id} # Get playlist (requires auth)
|
||||
- POST /api/players/{id}/heartbeat # Send status (requires auth)
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Test authentication
|
||||
./test_authentication.py
|
||||
|
||||
# Clear saved auth (re-authenticate)
|
||||
rm src/player_auth.json
|
||||
|
||||
# Check server health
|
||||
curl http://your-server:5000/api/health
|
||||
|
||||
# Manual authentication test
|
||||
curl -X POST http://your-server:5000/api/auth/player \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"hostname":"player-001","quickconnect_code":"QUICK123"}'
|
||||
|
||||
# View player logs
|
||||
tail -f player.log
|
||||
|
||||
# View server logs (if running Flask dev server)
|
||||
# Logs appear in terminal where server is running
|
||||
```
|
||||
|
||||
## Troubleshooting One-Liners
|
||||
|
||||
```bash
|
||||
# Authentication fails → Check player exists
|
||||
curl http://your-server:5000/api/health
|
||||
|
||||
# Auth expired → Clear and retry
|
||||
rm src/player_auth.json && python3 main.py
|
||||
|
||||
# Can't connect → Test network
|
||||
ping your-server-ip
|
||||
|
||||
# Wrong quickconnect → Check in DigiServer web UI
|
||||
# Go to: Players → [Your Player] → Edit → View Quick Connect Code
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- ✅ Auth code saved in `player_auth.json` (keep secure!)
|
||||
- ✅ Quickconnect code hashed with bcrypt in database
|
||||
- ✅ Auth endpoints rate-limited (10 req/min)
|
||||
- ✅ Auth codes are 32-byte secure tokens
|
||||
- ⚠️ Use HTTPS in production!
|
||||
- ⚠️ Rotate quickconnect codes periodically
|
||||
|
||||
## Quick Wins
|
||||
|
||||
### Before (Old System)
|
||||
- Every API call = send hostname + quickconnect
|
||||
- Server runs bcrypt check on every request
|
||||
- Slow response times
|
||||
- No session tracking
|
||||
|
||||
### After (New System)
|
||||
- Authenticate once = get auth_code
|
||||
- All subsequent calls use auth_code
|
||||
- 10x faster API responses
|
||||
- Server tracks player sessions
|
||||
- Can revoke access instantly
|
||||
|
||||
---
|
||||
|
||||
**Ready to go!** 🎉 Test with `./test_authentication.py` then start your player!
|
||||
@@ -1,152 +0,0 @@
|
||||
# Kivy Signage Player
|
||||
|
||||
A modern digital signage player built with Kivy framework that displays content from DigiServer playlists.
|
||||
|
||||
## Features
|
||||
|
||||
- **Cross-platform**: Runs on Linux, Windows, and macOS
|
||||
- **Modern UI**: Built with Kivy framework for smooth graphics and animations
|
||||
- **Multiple Media Types**: Supports images (JPG, PNG, GIF, BMP) and videos (MP4, AVI, MKV, MOV, WEBM)
|
||||
- **Server Integration**: Fetches playlists from DigiServer with automatic updates
|
||||
- **Player Feedback**: Reports status and playback information to server
|
||||
- **Fullscreen Display**: Optimized for digital signage displays
|
||||
- **Touch Controls**: Mouse/touch-activated control panel
|
||||
- **Auto-restart**: Continuous playlist looping
|
||||
- **Error Handling**: Robust error handling with server feedback
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Install system dependencies:**
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
./install.sh
|
||||
```
|
||||
|
||||
2. **Configure the player:**
|
||||
Edit `config/app_config.json` with your server details:
|
||||
```json
|
||||
{
|
||||
"server_ip": "your-server-ip",
|
||||
"port": "5000",
|
||||
"screen_name": "your-player-name",
|
||||
"quickconnect_key": "your-quickconnect-code"
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Recommended: Using Start Script (with Virtual Environment)
|
||||
```bash
|
||||
chmod +x start.sh
|
||||
./start.sh
|
||||
```
|
||||
This script automatically:
|
||||
- Activates the Python virtual environment
|
||||
- Checks for configuration
|
||||
- Starts the player
|
||||
|
||||
### Alternative: Using Run Script
|
||||
```bash
|
||||
chmod +x run_player.sh
|
||||
./run_player.sh
|
||||
```
|
||||
|
||||
### Manual Start
|
||||
```bash
|
||||
# With virtual environment
|
||||
source .venv/bin/activate
|
||||
cd src
|
||||
python3 main.py
|
||||
|
||||
# Without virtual environment
|
||||
cd src
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## Controls
|
||||
|
||||
- **Mouse/Touch Movement**: Shows control panel for 3 seconds
|
||||
- **Previous (⏮)**: Go to previous media item
|
||||
- **Pause/Play (⏸/▶)**: Toggle playback
|
||||
- **Next (⏭)**: Skip to next media item
|
||||
- **Settings (⚙)**: View player configuration and status
|
||||
- **Exit (⏻)**: Close the application
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
Kiwi-signage/
|
||||
├── src/
|
||||
│ ├── main.py # Main Kivy application
|
||||
│ └── get_playlists.py # Playlist management and server communication
|
||||
├── config/
|
||||
│ └── app_config.json # Player configuration
|
||||
├── media/ # Downloaded media files (auto-generated)
|
||||
├── playlists/ # Playlist cache (auto-generated)
|
||||
├── requirements.txt # Python dependencies
|
||||
├── install.sh # Installation script
|
||||
├── run_player.sh # Run script
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### app_config.json
|
||||
- `server_ip`: IP address or domain of DigiServer
|
||||
- `port`: Port number of DigiServer (default: 5000)
|
||||
- `screen_name`: Unique identifier for this player
|
||||
- `quickconnect_key`: Authentication key for server access
|
||||
|
||||
## Features Comparison with Tkinter Player
|
||||
|
||||
| Feature | Kivy Player | Tkinter Player |
|
||||
|---------|-------------|----------------|
|
||||
| Cross-platform | ✅ Better | ✅ Good |
|
||||
| Modern UI | ✅ Excellent | ❌ Basic |
|
||||
| Touch Support | ✅ Native | ❌ Limited |
|
||||
| Video Playback | ✅ Built-in | ✅ VLC Required |
|
||||
| Performance | ✅ GPU Accelerated | ❌ CPU Only |
|
||||
| Animations | ✅ Smooth | ❌ None |
|
||||
| Mobile Ready | ✅ Yes | ❌ No |
|
||||
|
||||
## Server Integration
|
||||
|
||||
The player communicates with DigiServer via REST API:
|
||||
|
||||
- **Playlist Fetch**: `GET /api/playlists`
|
||||
- **Player Feedback**: `POST /api/player-feedback`
|
||||
|
||||
Status updates sent to server:
|
||||
- Playlist check and update notifications
|
||||
- Current playback status
|
||||
- Error reports
|
||||
- Playlist restart notifications
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Installation Issues
|
||||
- Make sure system dependencies are installed: `./install.sh`
|
||||
- For ARM devices (Raspberry Pi), ensure proper SDL2 libraries
|
||||
|
||||
### Playback Issues
|
||||
- Check media file formats are supported
|
||||
- Verify network connection to DigiServer
|
||||
- Check player configuration in settings
|
||||
|
||||
### Server Connection
|
||||
- Verify server IP and port in configuration
|
||||
- Check quickconnect key is correct
|
||||
- Ensure DigiServer is running and accessible
|
||||
|
||||
## Development
|
||||
|
||||
Based on the proven architecture of the tkinter signage player with modern Kivy enhancements:
|
||||
|
||||
- **Playlist Management**: Inherited from `get_playlists.py`
|
||||
- **Media Playback**: Kivy's built-in Video and AsyncImage widgets
|
||||
- **Server Communication**: REST API calls with feedback system
|
||||
- **Error Handling**: Comprehensive exception handling with server reporting
|
||||
|
||||
## License
|
||||
|
||||
This project is part of the DigiServer digital signage system.
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Analyze what's happening with the playlist download."""
|
||||
|
||||
import json
|
||||
|
||||
# Check the saved playlist
|
||||
playlist_file = 'playlists/server_playlist_v8.json'
|
||||
print("=" * 80)
|
||||
print("SAVED PLAYLIST ANALYSIS")
|
||||
print("=" * 80)
|
||||
|
||||
with open(playlist_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f"\nVersion: {data.get('version', 'N/A')}")
|
||||
print(f"Items in playlist: {len(data.get('playlist', []))}")
|
||||
|
||||
print("\nPlaylist items:")
|
||||
for idx, item in enumerate(data.get('playlist', []), 1):
|
||||
print(f"\n{idx}. File: {item.get('file_name', 'N/A')}")
|
||||
print(f" URL: {item.get('url', 'N/A')}")
|
||||
print(f" Duration: {item.get('duration', 'N/A')}s")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("\n⚠️ ISSUE: Server has 5 files, but only 3 are saved!")
|
||||
print("\nPossible reasons:")
|
||||
print("1. Server sent only 3 files")
|
||||
print("2. 2 files failed to download and were skipped")
|
||||
print("3. Download function has a bug")
|
||||
print("\nThe download_media_files() function in get_playlists_v2.py:")
|
||||
print("- Downloads from the 'url' field in the playlist")
|
||||
print("- If download fails, it SKIPS the file (continues)")
|
||||
print("- Only successfully downloaded files are added to updated_playlist")
|
||||
print("\nThis means 2 files likely had invalid URLs or download errors!")
|
||||
print("=" * 80)
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Check video files for codec compatibility with ffpyplayer
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
def check_video_codec(video_path):
|
||||
"""Check video codec using ffprobe"""
|
||||
try:
|
||||
cmd = [
|
||||
'ffprobe',
|
||||
'-v', 'quiet',
|
||||
'-print_format', 'json',
|
||||
'-show_format',
|
||||
'-show_streams',
|
||||
video_path
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None, "ffprobe failed"
|
||||
|
||||
data = json.loads(result.stdout)
|
||||
|
||||
video_streams = [s for s in data.get('streams', []) if s.get('codec_type') == 'video']
|
||||
audio_streams = [s for s in data.get('streams', []) if s.get('codec_type') == 'audio']
|
||||
|
||||
if not video_streams:
|
||||
return None, "No video stream found"
|
||||
|
||||
video_stream = video_streams[0]
|
||||
|
||||
info = {
|
||||
'codec': video_stream.get('codec_name', 'unknown'),
|
||||
'codec_long': video_stream.get('codec_long_name', 'unknown'),
|
||||
'width': video_stream.get('width', 0),
|
||||
'height': video_stream.get('height', 0),
|
||||
'fps': eval(video_stream.get('r_frame_rate', '0/1')),
|
||||
'duration': float(data.get('format', {}).get('duration', 0)),
|
||||
'bitrate': int(data.get('format', {}).get('bit_rate', 0)),
|
||||
'audio_codec': audio_streams[0].get('codec_name', 'none') if audio_streams else 'none',
|
||||
'size': int(data.get('format', {}).get('size', 0))
|
||||
}
|
||||
|
||||
return info, None
|
||||
|
||||
except FileNotFoundError:
|
||||
return None, "ffprobe not installed (run: sudo apt-get install ffmpeg)"
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
def main():
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
media_dir = os.path.join(base_dir, 'media')
|
||||
|
||||
print("=" * 80)
|
||||
print("VIDEO CODEC COMPATIBILITY CHECKER")
|
||||
print("=" * 80)
|
||||
|
||||
# Supported codecs by ffpyplayer
|
||||
supported_codecs = ['h264', 'h265', 'hevc', 'vp8', 'vp9', 'mpeg4']
|
||||
|
||||
# Find video files
|
||||
video_extensions = ['.mp4', '.avi', '.mkv', '.mov', '.webm']
|
||||
video_files = []
|
||||
|
||||
if os.path.exists(media_dir):
|
||||
for filename in os.listdir(media_dir):
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext in video_extensions:
|
||||
video_files.append(filename)
|
||||
|
||||
if not video_files:
|
||||
print("\n✓ No video files found in media directory")
|
||||
return
|
||||
|
||||
print(f"\nFound {len(video_files)} video file(s):\n")
|
||||
|
||||
for filename in video_files:
|
||||
video_path = os.path.join(media_dir, filename)
|
||||
print(f"📹 {filename}")
|
||||
print(f" Path: {video_path}")
|
||||
|
||||
info, error = check_video_codec(video_path)
|
||||
|
||||
if error:
|
||||
print(f" ❌ ERROR: {error}")
|
||||
continue
|
||||
|
||||
# Display video info
|
||||
print(f" Video Codec: {info['codec']} ({info['codec_long']})")
|
||||
print(f" Resolution: {info['width']}x{info['height']}")
|
||||
print(f" Frame Rate: {info['fps']:.2f} fps")
|
||||
print(f" Duration: {info['duration']:.1f}s")
|
||||
print(f" Bitrate: {info['bitrate'] / 1000:.0f} kbps")
|
||||
print(f" Audio Codec: {info['audio_codec']}")
|
||||
print(f" File Size: {info['size'] / (1024*1024):.2f} MB")
|
||||
|
||||
# Check compatibility
|
||||
if info['codec'] in supported_codecs:
|
||||
print(f" ✅ COMPATIBLE - Codec '{info['codec']}' is supported by ffpyplayer")
|
||||
else:
|
||||
print(f" ⚠️ WARNING - Codec '{info['codec']}' may not be supported")
|
||||
print(f" Supported codecs: {', '.join(supported_codecs)}")
|
||||
print(f" Consider re-encoding to H.264:")
|
||||
print(f" ffmpeg -i \"{filename}\" -c:v libx264 -preset fast -crf 23 \"{os.path.splitext(filename)[0]}_h264.mp4\"")
|
||||
|
||||
# Performance warnings
|
||||
if info['width'] > 1920 or info['height'] > 1080:
|
||||
print(f" ⚠️ High resolution ({info['width']}x{info['height']}) may cause performance issues")
|
||||
print(f" Consider downscaling to 1920x1080 or lower")
|
||||
|
||||
if info['bitrate'] > 5000000: # 5 Mbps
|
||||
print(f" ⚠️ High bitrate ({info['bitrate'] / 1000000:.1f} Mbps) may cause playback issues")
|
||||
print(f" Consider reducing bitrate to 2-4 Mbps")
|
||||
|
||||
print()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Video Conversion Script for Raspberry Pi Signage Player
|
||||
# Converts videos to optimal settings: 1080p @ 30fps, H.264 codec
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <input_video> [output_video]"
|
||||
echo "Example: $0 input.mp4 output.mp4"
|
||||
echo ""
|
||||
echo "This script converts videos to Raspberry Pi-friendly settings:"
|
||||
echo " - Resolution: Max 1920x1080"
|
||||
echo " - Frame rate: 30 fps"
|
||||
echo " - Codec: H.264"
|
||||
echo " - Bitrate: ~5-8 Mbps"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT_VIDEO="$1"
|
||||
OUTPUT_VIDEO="${2:-converted_$(basename "$INPUT_VIDEO")}"
|
||||
|
||||
if [ ! -f "$INPUT_VIDEO" ]; then
|
||||
echo "Error: Input file '$INPUT_VIDEO' not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Converting video for Raspberry Pi playback..."
|
||||
echo "Input: $INPUT_VIDEO"
|
||||
echo "Output: $OUTPUT_VIDEO"
|
||||
echo ""
|
||||
|
||||
# Convert video with optimal settings for Raspberry Pi
|
||||
ffmpeg -i "$INPUT_VIDEO" \
|
||||
-c:v libx264 \
|
||||
-preset medium \
|
||||
-crf 23 \
|
||||
-maxrate 8M \
|
||||
-bufsize 12M \
|
||||
-vf "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease,fps=30" \
|
||||
-r 30 \
|
||||
-c:a aac \
|
||||
-b:a 128k \
|
||||
-movflags +faststart \
|
||||
-y \
|
||||
"$OUTPUT_VIDEO"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
echo "✓ Conversion completed successfully!"
|
||||
echo "Original: $(du -h "$INPUT_VIDEO" | cut -f1)"
|
||||
echo "Converted: $(du -h "$OUTPUT_VIDEO" | cut -f1)"
|
||||
echo ""
|
||||
echo "You can now use '$OUTPUT_VIDEO' in your signage player."
|
||||
else
|
||||
echo ""
|
||||
echo "✗ Conversion failed! Make sure ffmpeg is installed:"
|
||||
echo " sudo apt-get install ffmpeg"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,167 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Diagnostic script to check why media files might be skipped
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
# Paths
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
media_dir = os.path.join(base_dir, 'media')
|
||||
playlists_dir = os.path.join(base_dir, 'playlists')
|
||||
|
||||
# Supported extensions
|
||||
VIDEO_EXTENSIONS = ['.mp4', '.avi', '.mkv', '.mov', '.webm']
|
||||
IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
|
||||
SUPPORTED_EXTENSIONS = VIDEO_EXTENSIONS + IMAGE_EXTENSIONS
|
||||
|
||||
def check_playlist():
|
||||
"""Check playlist for issues"""
|
||||
print("=" * 80)
|
||||
print("PLAYLIST DIAGNOSTIC TOOL")
|
||||
print("=" * 80)
|
||||
|
||||
# Find latest playlist file
|
||||
playlist_files = [f for f in os.listdir(playlists_dir)
|
||||
if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
|
||||
if not playlist_files:
|
||||
print("\n❌ ERROR: No playlist files found!")
|
||||
return
|
||||
|
||||
# Sort by version and get latest
|
||||
versions = [(int(f.split('_v')[-1].split('.json')[0]), f) for f in playlist_files]
|
||||
versions.sort(reverse=True)
|
||||
latest_file = versions[0][1]
|
||||
playlist_path = os.path.join(playlists_dir, latest_file)
|
||||
|
||||
print(f"\n📋 Latest Playlist: {latest_file}")
|
||||
print(f" Path: {playlist_path}")
|
||||
|
||||
# Load playlist
|
||||
try:
|
||||
with open(playlist_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
playlist = data.get('playlist', [])
|
||||
version = data.get('version', 0)
|
||||
|
||||
print(f" Version: {version}")
|
||||
print(f" Total items: {len(playlist)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ ERROR loading playlist: {e}")
|
||||
return
|
||||
|
||||
# Check media directory
|
||||
print(f"\n📁 Media Directory: {media_dir}")
|
||||
if not os.path.exists(media_dir):
|
||||
print(" ❌ ERROR: Media directory doesn't exist!")
|
||||
return
|
||||
|
||||
media_files = os.listdir(media_dir)
|
||||
print(f" Files found: {len(media_files)}")
|
||||
for f in media_files:
|
||||
print(f" - {f}")
|
||||
|
||||
# Check each playlist item
|
||||
print("\n" + "=" * 80)
|
||||
print("CHECKING PLAYLIST ITEMS")
|
||||
print("=" * 80)
|
||||
|
||||
valid_count = 0
|
||||
missing_count = 0
|
||||
unsupported_count = 0
|
||||
|
||||
for idx, item in enumerate(playlist, 1):
|
||||
file_name = item.get('file_name', '')
|
||||
duration = item.get('duration', 0)
|
||||
media_path = os.path.join(media_dir, file_name)
|
||||
file_ext = os.path.splitext(file_name)[1].lower()
|
||||
|
||||
print(f"\n[{idx}/{len(playlist)}] {file_name}")
|
||||
print(f" Duration: {duration}s")
|
||||
|
||||
# Check if file exists
|
||||
if not os.path.exists(media_path):
|
||||
print(f" ❌ STATUS: FILE NOT FOUND")
|
||||
print(f" Expected path: {media_path}")
|
||||
missing_count += 1
|
||||
continue
|
||||
|
||||
# Check file size
|
||||
file_size = os.path.getsize(media_path)
|
||||
print(f" ✓ File exists ({file_size:,} bytes)")
|
||||
|
||||
# Check if supported type
|
||||
if file_ext not in SUPPORTED_EXTENSIONS:
|
||||
print(f" ❌ STATUS: UNSUPPORTED FILE TYPE '{file_ext}'")
|
||||
print(f" Supported extensions: {', '.join(SUPPORTED_EXTENSIONS)}")
|
||||
unsupported_count += 1
|
||||
continue
|
||||
|
||||
# Check media type
|
||||
if file_ext in VIDEO_EXTENSIONS:
|
||||
media_type = "VIDEO"
|
||||
elif file_ext in IMAGE_EXTENSIONS:
|
||||
media_type = "IMAGE"
|
||||
else:
|
||||
media_type = "UNKNOWN"
|
||||
|
||||
print(f" ✓ Type: {media_type}")
|
||||
print(f" ✓ Extension: {file_ext}")
|
||||
print(f" ✓ STATUS: SHOULD PLAY OK")
|
||||
valid_count += 1
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY")
|
||||
print("=" * 80)
|
||||
print(f"Total items: {len(playlist)}")
|
||||
print(f"✓ Valid: {valid_count}")
|
||||
print(f"❌ Missing files: {missing_count}")
|
||||
print(f"❌ Unsupported: {unsupported_count}")
|
||||
|
||||
if valid_count == len(playlist):
|
||||
print("\n✅ All playlist items should play correctly!")
|
||||
else:
|
||||
print(f"\n⚠️ WARNING: {len(playlist) - valid_count} items may be skipped!")
|
||||
|
||||
# Additional checks
|
||||
print("\n" + "=" * 80)
|
||||
print("ADDITIONAL CHECKS")
|
||||
print("=" * 80)
|
||||
|
||||
# Check for files in media dir not in playlist
|
||||
playlist_files_set = {item.get('file_name', '') for item in playlist}
|
||||
orphaned_files = [f for f in media_files if f not in playlist_files_set]
|
||||
|
||||
if orphaned_files:
|
||||
print(f"\n⚠️ Files in media directory NOT in playlist:")
|
||||
for f in orphaned_files:
|
||||
print(f" - {f}")
|
||||
else:
|
||||
print("\n✓ All media files are in the playlist")
|
||||
|
||||
# Check for case sensitivity issues
|
||||
print("\n🔍 Checking for case sensitivity issues...")
|
||||
media_files_lower = {f.lower(): f for f in media_files}
|
||||
case_issues = []
|
||||
|
||||
for item in playlist:
|
||||
file_name = item.get('file_name', '')
|
||||
if file_name.lower() in media_files_lower:
|
||||
actual_name = media_files_lower[file_name.lower()]
|
||||
if actual_name != file_name:
|
||||
case_issues.append((file_name, actual_name))
|
||||
|
||||
if case_issues:
|
||||
print("⚠️ Case sensitivity mismatches found:")
|
||||
for playlist_name, actual_name in case_issues:
|
||||
print(f" Playlist: {playlist_name}")
|
||||
print(f" Actual: {actual_name}")
|
||||
else:
|
||||
print("✓ No case sensitivity issues found")
|
||||
|
||||
if __name__ == '__main__':
|
||||
check_playlist()
|
||||
@@ -1,125 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Download DEB Packages Script for Offline Installation
|
||||
# This script downloads all system .deb packages required for offline installation
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SYSTEM_DIR="$SCRIPT_DIR/repo/system-packages"
|
||||
DEB_DIR="$SYSTEM_DIR/debs"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Downloading DEB Packages for Offline Install"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Create debs directory
|
||||
mkdir -p "$DEB_DIR"
|
||||
|
||||
# Check if running on Debian/Ubuntu/Raspberry Pi OS
|
||||
if ! command -v apt-get &> /dev/null; then
|
||||
echo "Error: This script requires apt-get (Debian/Ubuntu/Raspberry Pi OS)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Reading package list from: $SYSTEM_DIR/apt-packages.txt"
|
||||
echo ""
|
||||
|
||||
# Update package cache
|
||||
echo "Updating package cache..."
|
||||
sudo apt update
|
||||
|
||||
# Read packages from file
|
||||
PACKAGES=$(grep -v '^#' "$SYSTEM_DIR/apt-packages.txt" | grep -v '^$' | tr '\n' ' ')
|
||||
|
||||
echo "Packages to download:"
|
||||
echo "$PACKAGES"
|
||||
echo ""
|
||||
|
||||
# Download packages and dependencies
|
||||
echo "Downloading packages with dependencies..."
|
||||
cd "$DEB_DIR"
|
||||
|
||||
# Use apt-get download to get .deb files
|
||||
for pkg in $PACKAGES; do
|
||||
echo "Downloading: $pkg"
|
||||
apt-get download "$pkg" 2>/dev/null || echo " Warning: Could not download $pkg"
|
||||
done
|
||||
|
||||
# Download dependencies
|
||||
echo ""
|
||||
echo "Downloading dependencies..."
|
||||
sudo apt-get install --download-only --reinstall -y $PACKAGES
|
||||
|
||||
# Copy downloaded debs from apt cache
|
||||
echo ""
|
||||
echo "Copying packages from apt cache..."
|
||||
sudo cp /var/cache/apt/archives/*.deb "$DEB_DIR/" 2>/dev/null || true
|
||||
|
||||
# Remove duplicate packages
|
||||
echo ""
|
||||
echo "Removing duplicates..."
|
||||
cd "$DEB_DIR"
|
||||
for file in *.deb; do
|
||||
[ -f "$file" ] || continue
|
||||
basename="${file%%_*}"
|
||||
count=$(ls -1 "${basename}"_*.deb 2>/dev/null | wc -l)
|
||||
if [ "$count" -gt 1 ]; then
|
||||
# Keep only the latest version
|
||||
ls -t "${basename}"_*.deb | tail -n +2 | xargs rm -f
|
||||
fi
|
||||
done
|
||||
|
||||
# Create installation order file
|
||||
echo ""
|
||||
echo "Creating installation order..."
|
||||
cat > "$DEB_DIR/install-order.txt" << 'EOF'
|
||||
# Install packages in this order to resolve dependencies
|
||||
|
||||
# 1. Base tools and libraries
|
||||
python3-pip_*.deb
|
||||
python3-setuptools_*.deb
|
||||
python3-dev_*.deb
|
||||
zlib1g-dev_*.deb
|
||||
|
||||
# 2. SDL2 libraries
|
||||
libsdl2-dev_*.deb
|
||||
libsdl2-image-dev_*.deb
|
||||
libsdl2-mixer-dev_*.deb
|
||||
libsdl2-ttf-dev_*.deb
|
||||
|
||||
# 3. Multimedia libraries
|
||||
libportmidi-dev_*.deb
|
||||
libswscale-dev_*.deb
|
||||
libavformat-dev_*.deb
|
||||
libavcodec-dev_*.deb
|
||||
libavcodec-extra_*.deb
|
||||
|
||||
# 4. FFmpeg and codecs
|
||||
ffmpeg_*.deb
|
||||
libx264-dev_*.deb
|
||||
|
||||
# 5. GStreamer
|
||||
gstreamer1.0-plugins-base_*.deb
|
||||
gstreamer1.0-plugins-good_*.deb
|
||||
gstreamer1.0-plugins-bad_*.deb
|
||||
gstreamer1.0-alsa_*.deb
|
||||
|
||||
# 6. Network tools
|
||||
wget_*.deb
|
||||
curl_*.deb
|
||||
EOF
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "DEB Download Complete!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Downloaded .deb files: $(ls -1 *.deb 2>/dev/null | wc -l)"
|
||||
echo "Location: $DEB_DIR"
|
||||
echo ""
|
||||
echo "To install offline, copy the repo folder and run:"
|
||||
echo " bash install.sh --offline"
|
||||
echo ""
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Download Offline Packages Script for Kivy Signage Player
|
||||
# This script downloads all necessary Python packages and documents system packages
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$SCRIPT_DIR/repo"
|
||||
WHEELS_DIR="$REPO_DIR/python-wheels"
|
||||
SYSTEM_DIR="$REPO_DIR/system-packages"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Downloading Offline Packages"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check if repo directory exists
|
||||
if [ ! -d "$REPO_DIR" ]; then
|
||||
echo "Error: repo directory not found!"
|
||||
echo "Creating directories..."
|
||||
mkdir -p "$WHEELS_DIR"
|
||||
mkdir -p "$SYSTEM_DIR"
|
||||
fi
|
||||
|
||||
# Download Python packages
|
||||
echo "Step 1: Downloading Python wheels..."
|
||||
echo "--------------------"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Check if pip is installed
|
||||
if ! command -v pip3 &> /dev/null; then
|
||||
echo "Error: pip3 is not installed. Please install it first:"
|
||||
echo " sudo apt install python3-pip"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download all Python packages and their dependencies
|
||||
echo "Downloading packages from requirements.txt..."
|
||||
pip3 download -r requirements.txt -d "$WHEELS_DIR" --platform linux_armv7l --only-binary=:all: || \
|
||||
pip3 download -r requirements.txt -d "$WHEELS_DIR" || true
|
||||
|
||||
# Also download for general Linux platforms as fallback
|
||||
echo "Downloading cross-platform packages..."
|
||||
pip3 download -r requirements.txt -d "$WHEELS_DIR" || true
|
||||
|
||||
echo ""
|
||||
echo "Python wheels downloaded to: $WHEELS_DIR"
|
||||
echo "Total wheel files: $(ls -1 "$WHEELS_DIR"/*.whl 2>/dev/null | wc -l)"
|
||||
|
||||
# List system packages
|
||||
echo ""
|
||||
echo "Step 2: System packages information"
|
||||
echo "--------------------"
|
||||
echo "System packages are listed in: $SYSTEM_DIR/apt-packages.txt"
|
||||
echo ""
|
||||
echo "To download .deb files for offline installation, run:"
|
||||
echo " bash download_deb_packages.sh"
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "=========================================="
|
||||
echo "Download Complete!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Offline packages ready in: $REPO_DIR"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Copy the entire 'repo' folder to your offline system"
|
||||
echo "2. Run: bash install.sh"
|
||||
echo " (The installer will automatically detect and use offline packages)"
|
||||
echo ""
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Execute the exact server playlist retrieval flow the player performs:
|
||||
1. Load config/app_config.json (player code: screen_name + quickconnect_key)
|
||||
2. Authenticate with the server (POST /api/auth/player)
|
||||
3. Fetch playlist using the player_id + auth_code (GET /api/playlists/{player_id})
|
||||
4. Print the RAW JSON exactly as received (before any local processing)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import traceback
|
||||
|
||||
# Resolve the real src directory (script lives in working_files/, source is in src/)
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SRC_DIR = os.path.join(os.path.dirname(HERE), 'src')
|
||||
sys.path.insert(0, SRC_DIR)
|
||||
|
||||
from get_playlists_v2 import get_auth_instance # noqa: E402
|
||||
from player_auth import PlayerAuth # noqa: E402
|
||||
|
||||
CONFIG_PATH = os.path.join(os.path.dirname(HERE), 'config', 'app_config.json')
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 80)
|
||||
print("EXECUTE SERVER PLAYLIST RETRIEVAL (player flow)")
|
||||
print("=" * 80)
|
||||
|
||||
# 1. Load the player config
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
print(f"\n[1] Player config loaded from: {CONFIG_PATH}")
|
||||
print(f" server_ip : {config.get('server_ip')}")
|
||||
print(f" port : {config.get('port')}")
|
||||
print(f" screen_name : {config.get('screen_name')} <- player code")
|
||||
print(f" quickconnect_key: {config.get('quickconnect_key')} <- player quick-connect code")
|
||||
print(f" use_https : {config.get('use_https')}")
|
||||
print(f" verify_ssl : {config.get('verify_ssl')}")
|
||||
|
||||
# 2. Build server URL the same way ensure_authenticated() does
|
||||
import re
|
||||
server_ip = config.get("server_ip", "")
|
||||
port = config.get("port", "")
|
||||
use_https = config.get("use_https", True)
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server_ip):
|
||||
if use_https:
|
||||
server_url = f'https://{server_ip}:{port}' if port else f'https://{server_ip}'
|
||||
else:
|
||||
server_url = f'http://{server_ip}:{port}' if port else f'http://{server_ip}'
|
||||
else:
|
||||
server_url = f'https://{server_ip}' if use_https else f'http://{server_ip}'
|
||||
print(f"\n[2] Server URL used: {server_url}")
|
||||
|
||||
# 3. Authenticate with the player code
|
||||
auth = get_auth_instance(
|
||||
config_file=os.path.join(SRC_DIR, 'player_auth.json'),
|
||||
use_https=config.get('use_https', True),
|
||||
verify_ssl=config.get('verify_ssl', True)
|
||||
)
|
||||
|
||||
print(f"\n[3] Authenticating player '{config.get('screen_name')}' with quickconnect code...")
|
||||
success, error = auth.authenticate(
|
||||
server_url=server_url,
|
||||
hostname=config.get('screen_name', ''),
|
||||
quickconnect_code=config.get('quickconnect_key', '')
|
||||
)
|
||||
if not success:
|
||||
print(f" AUTH FAILED: {error}")
|
||||
print(" Cannot retrieve playlist without a valid player code/auth.")
|
||||
return
|
||||
print(f" Authenticated as: {auth.get_player_name()} (player_id={auth.get_player_id()}, "
|
||||
f"playlist_id={auth.auth_data.get('playlist_id')})")
|
||||
|
||||
# 4. Fetch the playlist (GET /api/playlists/{player_id})
|
||||
print(f"\n[4] Fetching playlist from: {server_url}/api/playlists/{auth.get_player_id()}")
|
||||
playlist_data = auth.get_playlist()
|
||||
|
||||
if playlist_data is None:
|
||||
print(" PLAYLIST FETCH FAILED")
|
||||
return
|
||||
|
||||
# 5. Print the RAW JSON exactly as received from the server
|
||||
print(f"\n[5] RAW JSON received from server (status 200):\n")
|
||||
print(json.dumps(playlist_data, indent=2, ensure_ascii=False))
|
||||
|
||||
# 6. Save the raw response for inspection
|
||||
out_path = os.path.join(HERE, 'raw_server_playlist.json')
|
||||
with open(out_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(playlist_data, f, indent=2, ensure_ascii=False)
|
||||
print(f"\n[6] Raw server response saved to: {out_path}")
|
||||
|
||||
# 7. Summary
|
||||
print("\n" + "=" * 80)
|
||||
print(f"SUMMARY: playlist_version={playlist_data.get('playlist_version')}, "
|
||||
f"count={playlist_data.get('count')}, items={len(playlist_data.get('playlist', []))}")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Force playlist update to download all files."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from get_playlists_v2 import update_playlist_if_needed
|
||||
|
||||
# Load config
|
||||
config_file = 'config/app_config.json'
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
print("=" * 80)
|
||||
print("FORCING PLAYLIST UPDATE")
|
||||
print("=" * 80)
|
||||
|
||||
playlist_dir = 'playlists'
|
||||
media_dir = 'media'
|
||||
|
||||
print(f"\nConfiguration:")
|
||||
print(f" Playlist dir: {playlist_dir}")
|
||||
print(f" Media dir: {media_dir}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("Updating playlist...")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
result = update_playlist_if_needed(config, playlist_dir, media_dir)
|
||||
|
||||
if result:
|
||||
print("\n" + "=" * 80)
|
||||
print("SUCCESS!")
|
||||
print("=" * 80)
|
||||
print(f"✓ Playlist updated to: {result}")
|
||||
|
||||
# Check media directory
|
||||
import os
|
||||
media_files = sorted([f for f in os.listdir(media_dir) if not f.startswith('.')])
|
||||
print(f"\n✓ Media files downloaded ({len(media_files)}):")
|
||||
for f in media_files:
|
||||
size = os.path.getsize(os.path.join(media_dir, f))
|
||||
print(f" - {f} ({size:,} bytes)")
|
||||
|
||||
else:
|
||||
print("\n" + "=" * 80)
|
||||
print("FAILED or already up to date")
|
||||
print("=" * 80)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
@@ -1,346 +0,0 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import bcrypt
|
||||
import re
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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
|
||||
# Remove protocol if already present
|
||||
server_clean = server.replace('http://', '').replace('https://', '')
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server_clean):
|
||||
feedback_url = f'http://{server_clean}:{port}/api/player-feedback'
|
||||
else:
|
||||
# Use original server if it has protocol, otherwise add http://
|
||||
if server.startswith(('http://', 'https://')):
|
||||
feedback_url = f'{server}/api/player-feedback'
|
||||
else:
|
||||
feedback_url = f'http://{server}/api/player-feedback'
|
||||
|
||||
# Prepare feedback data
|
||||
feedback_data = {
|
||||
'hostname': 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}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Feedback failed with status {response.status_code}: {response.text}")
|
||||
return False
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to send feedback: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error sending feedback: {e}")
|
||||
return False
|
||||
|
||||
def send_playlist_check_feedback(config, playlist_version=None):
|
||||
"""
|
||||
Send feedback when playlist is checked for 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"v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, is active, Playing {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"v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, playlist loop completed, restarting {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 current playing status.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
current_media (str, optional): Currently playing media file
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"v{playlist_version}" if playlist_version else "unknown"
|
||||
media_info = f" - {current_media}" if current_media else ""
|
||||
message = f"player {player_name}, is active, Playing {version_info}{media_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="playing",
|
||||
playlist_version=playlist_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:
|
||||
# Remove protocol if already present
|
||||
server_clean = server.replace('http://', '').replace('https://', '')
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server_clean):
|
||||
server_url = f'http://{server_clean}:{port}/api/playlists'
|
||||
else:
|
||||
# Use original server if it has protocol, otherwise add http://
|
||||
if server.startswith(('http://', 'https://')):
|
||||
server_url = f'{server}/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)
|
||||
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.")
|
||||
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 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)
|
||||
logger.info(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.
|
||||
"""
|
||||
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}")
|
||||
|
||||
# Send feedback about playlist check
|
||||
send_playlist_check_feedback(config, server_version if server_version > 0 else local_version)
|
||||
|
||||
if local_version != server_version:
|
||||
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)
|
||||
|
||||
# Send feedback about playlist update
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
update_message = f"player {player_name}, playlist updated to v{server_version}"
|
||||
send_player_feedback(config, update_message, "active", server_version)
|
||||
|
||||
return True
|
||||
else:
|
||||
logger.warning("No playlist data fetched from server or playlist is empty.")
|
||||
|
||||
# Send error feedback
|
||||
send_player_error_feedback(config, "No playlist data fetched from server or playlist is empty", local_version)
|
||||
|
||||
return False
|
||||
else:
|
||||
logger.info("Local playlist is already up to date.")
|
||||
return False
|
||||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"count": 6,
|
||||
"player_id": 2,
|
||||
"player_name": "Windows-Player1",
|
||||
"playlist": [
|
||||
{
|
||||
"audio": "off",
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"edit_on_player": false,
|
||||
"file_name": "anders-jilden-cYrMQA7a3Wc-unsplash.jpg",
|
||||
"id": 9,
|
||||
"muted": true,
|
||||
"position": 1,
|
||||
"type": "image",
|
||||
"url": "http://192.168.0.107:8080/static/uploads/anders-jilden-cYrMQA7a3Wc-unsplash.jpg"
|
||||
},
|
||||
{
|
||||
"audio": "off",
|
||||
"description": null,
|
||||
"duration": 14,
|
||||
"edit_on_player": true,
|
||||
"file_name": "sean-oulashin-KMn4VEeEPR8-unsplash.jpg",
|
||||
"id": 2,
|
||||
"muted": true,
|
||||
"position": 2,
|
||||
"type": "image",
|
||||
"url": "http://192.168.0.107:8080/static/uploads/sean-oulashin-KMn4VEeEPR8-unsplash.jpg"
|
||||
},
|
||||
{
|
||||
"audio": "on",
|
||||
"description": null,
|
||||
"duration": 31,
|
||||
"edit_on_player": false,
|
||||
"file_name": "sample-30s.mp4",
|
||||
"id": 4,
|
||||
"muted": false,
|
||||
"position": 3,
|
||||
"type": "video",
|
||||
"url": "http://192.168.0.107:8080/static/uploads/sample-30s.mp4"
|
||||
},
|
||||
{
|
||||
"audio": "off",
|
||||
"description": null,
|
||||
"duration": 50,
|
||||
"edit_on_player": true,
|
||||
"file_name": "edited_media/5/eye_e_v2.jpg",
|
||||
"id": 5,
|
||||
"muted": true,
|
||||
"position": 4,
|
||||
"type": "image",
|
||||
"url": "http://192.168.0.107:8080/static/uploads/edited_media/5/eye_e_v2.jpg"
|
||||
},
|
||||
{
|
||||
"audio": "off",
|
||||
"description": "https://moto-adv.com/",
|
||||
"duration": 30,
|
||||
"edit_on_player": false,
|
||||
"file_name": "weblink-ecc2705c34e5",
|
||||
"id": 7,
|
||||
"muted": true,
|
||||
"position": 5,
|
||||
"type": "weblink",
|
||||
"url": "https://moto-adv.com/"
|
||||
},
|
||||
{
|
||||
"audio": "off",
|
||||
"description": null,
|
||||
"duration": 30,
|
||||
"edit_on_player": false,
|
||||
"file_name": "jack-anstey-XVoyX7l9ocY-unsplash.jpg",
|
||||
"id": 8,
|
||||
"muted": true,
|
||||
"position": 6,
|
||||
"type": "image",
|
||||
"url": "http://192.168.0.107:8080/static/uploads/jack-anstey-XVoyX7l9ocY-unsplash.jpg"
|
||||
}
|
||||
],
|
||||
"playlist_id": 1,
|
||||
"playlist_version": 32
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"count": 5,
|
||||
"player_id": 1,
|
||||
"player_name": "TV-acasa 1",
|
||||
"playlist": [
|
||||
{
|
||||
"description": null,
|
||||
"duration": 15,
|
||||
"file_name": "music.jpg",
|
||||
"id": 1,
|
||||
"position": 1,
|
||||
"type": "image",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/music.jpg"
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"duration": 23,
|
||||
"file_name": "130414-746934884.mp4",
|
||||
"id": 2,
|
||||
"position": 3,
|
||||
"type": "video",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/130414-746934884.mp4"
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"duration": 15,
|
||||
"file_name": "IMG_0386.jpeg",
|
||||
"id": 4,
|
||||
"position": 4,
|
||||
"type": "image",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/IMG_0386.jpeg"
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"duration": 15,
|
||||
"file_name": "AGC_20250704_204105932.jpg",
|
||||
"id": 5,
|
||||
"position": 5,
|
||||
"type": "image",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/AGC_20250704_204105932.jpg"
|
||||
},
|
||||
{
|
||||
"description": null,
|
||||
"duration": 15,
|
||||
"file_name": "50194.jpg",
|
||||
"id": 3,
|
||||
"position": 6,
|
||||
"type": "image",
|
||||
"url": "http://digi-signage.moto-adv.com/static/uploads/50194.jpg"
|
||||
}
|
||||
],
|
||||
"playlist_id": 1,
|
||||
"playlist_version": 9
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for Kiwy-Signage authentication with DigiServer v2
|
||||
Run this to verify authentication is working before updating main.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from player_auth import PlayerAuth
|
||||
import json
|
||||
|
||||
def load_app_config():
|
||||
"""Load existing app_config.json"""
|
||||
# Try multiple possible locations
|
||||
possible_paths = [
|
||||
'config/app_config.json',
|
||||
'resources/app_config.txt',
|
||||
'src/config/app_config.json',
|
||||
'../config/app_config.json'
|
||||
]
|
||||
|
||||
for config_file in possible_paths:
|
||||
if os.path.exists(config_file):
|
||||
print(f" Found config: {config_file}")
|
||||
with open(config_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
print(f"❌ Config file not found! Tried:")
|
||||
for path in possible_paths:
|
||||
print(f" - {path}")
|
||||
return None
|
||||
|
||||
def test_authentication():
|
||||
"""Test authentication with DigiServer v2"""
|
||||
print("=" * 60)
|
||||
print("Kiwy-Signage Authentication Test")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Load config
|
||||
print("📁 Loading configuration...")
|
||||
config = load_app_config()
|
||||
if not config:
|
||||
return False
|
||||
|
||||
server_ip = config.get('server_ip', '')
|
||||
hostname = config.get('screen_name', '')
|
||||
quickconnect = config.get('quickconnect_key', '')
|
||||
port = config.get('port', '')
|
||||
|
||||
print(f" Server: {server_ip}:{port}")
|
||||
print(f" Hostname: {hostname}")
|
||||
print(f" Quick Connect: {'*' * len(quickconnect)}")
|
||||
print()
|
||||
|
||||
# Build server URL
|
||||
import re
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server_ip):
|
||||
server_url = f'http://{server_ip}:{port}'
|
||||
else:
|
||||
server_url = f'http://{server_ip}'
|
||||
|
||||
print(f"🌐 Server URL: {server_url}")
|
||||
print()
|
||||
|
||||
# Test server connection
|
||||
print("🔌 Testing server connection...")
|
||||
try:
|
||||
import requests
|
||||
response = requests.get(f"{server_url}/api/health", timeout=5)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ Server is healthy (version: {data.get('version')})")
|
||||
else:
|
||||
print(f" ⚠️ Server responded with status: {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Cannot connect to server: {e}")
|
||||
print()
|
||||
print("💡 Make sure DigiServer v2 is running and accessible!")
|
||||
return False
|
||||
print()
|
||||
|
||||
# Initialize auth
|
||||
print("🔐 Initializing authentication...")
|
||||
auth = PlayerAuth(config_file='src/player_auth.json')
|
||||
|
||||
# Check if already authenticated
|
||||
if auth.is_authenticated():
|
||||
print(f" ℹ️ Found existing authentication")
|
||||
print(f" Player: {auth.get_player_name()}")
|
||||
print()
|
||||
|
||||
print("✓ Verifying saved authentication...")
|
||||
valid, info = auth.verify_auth()
|
||||
|
||||
if valid:
|
||||
print(f" ✅ Authentication is valid!")
|
||||
print(f" Player ID: {info['player_id']}")
|
||||
print(f" Player Name: {info['player_name']}")
|
||||
print(f" Group ID: {info.get('group_id', 'None')}")
|
||||
print(f" Orientation: {info.get('orientation', 'Landscape')}")
|
||||
print()
|
||||
|
||||
# Test playlist fetch
|
||||
print("📋 Testing playlist fetch...")
|
||||
playlist_data = auth.get_playlist()
|
||||
if playlist_data:
|
||||
version = playlist_data.get('playlist_version', 0)
|
||||
content_count = len(playlist_data.get('playlist', []))
|
||||
print(f" ✅ Playlist received!")
|
||||
print(f" Version: {version}")
|
||||
print(f" Content items: {content_count}")
|
||||
else:
|
||||
print(f" ⚠️ Could not fetch playlist")
|
||||
print()
|
||||
|
||||
# Test heartbeat
|
||||
print("💓 Testing heartbeat...")
|
||||
if auth.send_heartbeat(status='online'):
|
||||
print(f" ✅ Heartbeat sent successfully")
|
||||
else:
|
||||
print(f" ⚠️ Heartbeat failed")
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print("✅ All tests passed! Player is ready to use.")
|
||||
print("=" * 60)
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ Saved authentication is expired or invalid")
|
||||
print(f" Re-authenticating...")
|
||||
print()
|
||||
|
||||
# Need to authenticate
|
||||
print("🔑 Authenticating with server...")
|
||||
success, error = auth.authenticate(
|
||||
server_url=server_url,
|
||||
hostname=hostname,
|
||||
quickconnect_code=quickconnect
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f" ✅ Authentication successful!")
|
||||
print(f" Player: {auth.get_player_name()}")
|
||||
print(f" Player ID: {auth.get_player_id()}")
|
||||
print()
|
||||
|
||||
# Save confirmation
|
||||
print(f"💾 Authentication saved to: src/player_auth.json")
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print("✅ Authentication successful! Player is ready to use.")
|
||||
print("=" * 60)
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ Authentication failed: {error}")
|
||||
print()
|
||||
print("💡 Troubleshooting:")
|
||||
print(" 1. Check player exists in DigiServer v2 (hostname must match)")
|
||||
print(" 2. Verify quickconnect_key matches server configuration")
|
||||
print(" 3. Check server logs for authentication attempts")
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("❌ Authentication test failed")
|
||||
print("=" * 60)
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
success = test_authentication()
|
||||
sys.exit(0 if success else 1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ Test cancelled by user")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Unexpected error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -1,162 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for USB card reader functionality
|
||||
"""
|
||||
|
||||
import evdev
|
||||
from evdev import InputDevice, categorize, ecodes
|
||||
import time
|
||||
|
||||
def list_input_devices():
|
||||
"""List all available input devices"""
|
||||
print("\n=== Available Input Devices ===")
|
||||
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
|
||||
|
||||
# Exclusion keywords that help identify non-card-reader devices
|
||||
exclusion_keywords = [
|
||||
'touch', 'touchscreen', 'mouse', 'mice', 'trackpad',
|
||||
'touchpad', 'pen', 'stylus', 'video', 'button', 'lid'
|
||||
]
|
||||
|
||||
for i, device in enumerate(devices):
|
||||
device_name_lower = device.name.lower()
|
||||
is_excluded = any(keyword in device_name_lower for keyword in exclusion_keywords)
|
||||
is_likely_card = 'card' in device_name_lower or 'reader' in device_name_lower or 'rfid' in device_name_lower
|
||||
|
||||
print(f"\n[{i}] {device.path}")
|
||||
print(f" Name: {device.name}")
|
||||
print(f" Phys: {device.phys}")
|
||||
|
||||
capabilities = device.capabilities()
|
||||
if ecodes.EV_KEY in capabilities:
|
||||
print(f" Type: Keyboard/HID Input Device")
|
||||
|
||||
# Add helpful hints
|
||||
if is_likely_card:
|
||||
print(f" ** LIKELY CARD READER **")
|
||||
elif is_excluded:
|
||||
print(f" (Excluded: appears to be touch/mouse/other non-card device)")
|
||||
elif 'usb' in device_name_lower and 'keyboard' in device_name_lower:
|
||||
print(f" (USB Keyboard - could be card reader)")
|
||||
|
||||
return devices
|
||||
|
||||
def test_card_reader(device_index=None):
|
||||
"""Test reading from a card reader device"""
|
||||
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
|
||||
|
||||
# Exclusion keywords (same as in main app)
|
||||
exclusion_keywords = [
|
||||
'touch', 'touchscreen', 'mouse', 'mice', 'trackpad',
|
||||
'touchpad', 'pen', 'stylus', 'video', 'button', 'lid'
|
||||
]
|
||||
|
||||
if device_index is not None:
|
||||
if device_index >= len(devices):
|
||||
print(f"Error: Device index {device_index} out of range")
|
||||
return
|
||||
device = devices[device_index]
|
||||
else:
|
||||
# Try to find a card reader automatically using same logic as main app
|
||||
device = None
|
||||
|
||||
# Priority 1: Explicit card readers
|
||||
for dev in devices:
|
||||
device_name_lower = dev.name.lower()
|
||||
if any(keyword in device_name_lower for keyword in exclusion_keywords):
|
||||
continue
|
||||
if 'card' in device_name_lower or 'reader' in device_name_lower or 'rfid' in device_name_lower or 'hid' in device_name_lower:
|
||||
capabilities = dev.capabilities()
|
||||
if ecodes.EV_KEY in capabilities:
|
||||
device = dev
|
||||
print(f"Found card reader: {dev.name}")
|
||||
break
|
||||
|
||||
# Priority 2: USB keyboards
|
||||
if not device:
|
||||
for dev in devices:
|
||||
device_name_lower = dev.name.lower()
|
||||
if any(keyword in device_name_lower for keyword in exclusion_keywords):
|
||||
continue
|
||||
if 'usb' in device_name_lower and 'keyboard' in device_name_lower:
|
||||
capabilities = dev.capabilities()
|
||||
if ecodes.EV_KEY in capabilities:
|
||||
device = dev
|
||||
print(f"Using USB keyboard as card reader: {dev.name}")
|
||||
break
|
||||
|
||||
# Priority 3: Any non-excluded keyboard
|
||||
if not device:
|
||||
for dev in devices:
|
||||
device_name_lower = dev.name.lower()
|
||||
if any(keyword in device_name_lower for keyword in exclusion_keywords):
|
||||
continue
|
||||
capabilities = dev.capabilities()
|
||||
if ecodes.EV_KEY in capabilities:
|
||||
device = dev
|
||||
print(f"Using keyboard device as card reader: {dev.name}")
|
||||
break
|
||||
|
||||
if not device:
|
||||
print("No suitable input device found!")
|
||||
return
|
||||
|
||||
print(f"\n=== Testing Card Reader ===")
|
||||
print(f"Device: {device.name}")
|
||||
print(f"Path: {device.path}")
|
||||
print("\nSwipe your card now (press Ctrl+C to exit)...\n")
|
||||
|
||||
card_data = ""
|
||||
|
||||
try:
|
||||
for event in device.read_loop():
|
||||
if event.type == ecodes.EV_KEY:
|
||||
key_event = categorize(event)
|
||||
|
||||
if key_event.keystate == 1: # Key down
|
||||
key_code = key_event.keycode
|
||||
|
||||
# Handle Enter key (card read complete)
|
||||
if key_code == 'KEY_ENTER':
|
||||
print(f"\n✓ Card data received: '{card_data}'")
|
||||
print(f" Length: {len(card_data)} characters")
|
||||
print(f" Processed ID: card_{card_data.strip().upper()}")
|
||||
print("\nReady for next card swipe...")
|
||||
card_data = ""
|
||||
|
||||
# Build card data string
|
||||
elif key_code.startswith('KEY_'):
|
||||
char = key_code.replace('KEY_', '')
|
||||
if len(char) == 1: # Single character
|
||||
card_data += char
|
||||
print(f"Reading: {card_data}", end='\r', flush=True)
|
||||
elif char.isdigit(): # Handle numeric keys
|
||||
card_data += char
|
||||
print(f"Reading: {card_data}", end='\r', flush=True)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nTest stopped by user")
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("USB Card Reader Test Tool")
|
||||
print("=" * 50)
|
||||
|
||||
devices = list_input_devices()
|
||||
|
||||
if not devices:
|
||||
print("\nNo input devices found!")
|
||||
exit(1)
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
choice = input("\nEnter device number to test (or press Enter for auto-detect): ").strip()
|
||||
|
||||
if choice:
|
||||
try:
|
||||
device_index = int(choice)
|
||||
test_card_reader(device_index)
|
||||
except ValueError:
|
||||
print("Invalid device number!")
|
||||
else:
|
||||
test_card_reader()
|
||||
@@ -1,138 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test server connection and playlist fetch."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from player_auth import PlayerAuth
|
||||
|
||||
# Load config
|
||||
config_file = 'config/app_config.json'
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
print("=" * 80)
|
||||
print("SERVER CONNECTION TEST")
|
||||
print("=" * 80)
|
||||
|
||||
server_ip = config.get("server_ip", "")
|
||||
screen_name = config.get("screen_name", "")
|
||||
quickconnect_key = config.get("quickconnect_key", "")
|
||||
port = config.get("port", "")
|
||||
|
||||
print(f"\nConfiguration:")
|
||||
print(f" Server: {server_ip}")
|
||||
print(f" Port: {port}")
|
||||
print(f" Screen Name: {screen_name}")
|
||||
print(f" QuickConnect: {quickconnect_key}")
|
||||
|
||||
# Build server URL
|
||||
if server_ip.startswith('http://') or server_ip.startswith('https://'):
|
||||
server_url = server_ip
|
||||
# If it has https but port 443 is specified, ensure port is included if non-standard
|
||||
if not ':' in server_ip.replace('https://', '').replace('http://', ''):
|
||||
if port and port != '443' and port != '80':
|
||||
server_url = f"{server_ip}:{port}"
|
||||
else:
|
||||
# Use https for port 443, http for others
|
||||
protocol = "https" if port == "443" else "http"
|
||||
server_url = f"{protocol}://{server_ip}:{port}"
|
||||
|
||||
print(f"\nServer URL: {server_url}")
|
||||
|
||||
# Test authentication
|
||||
print("\n" + "=" * 80)
|
||||
print("1. TESTING AUTHENTICATION")
|
||||
print("=" * 80)
|
||||
|
||||
auth = PlayerAuth('src/player_auth.json')
|
||||
|
||||
# Check if already authenticated
|
||||
if auth.is_authenticated():
|
||||
print("✓ Found existing authentication")
|
||||
valid, message = auth.verify_auth()
|
||||
if valid:
|
||||
print(f"✓ Auth is valid: {message}")
|
||||
else:
|
||||
print(f"✗ Auth expired: {message}")
|
||||
print("\nRe-authenticating...")
|
||||
success, error = auth.authenticate(
|
||||
server_url=server_url,
|
||||
hostname=screen_name,
|
||||
quickconnect_code=quickconnect_key
|
||||
)
|
||||
if success:
|
||||
print(f"✓ Re-authentication successful!")
|
||||
else:
|
||||
print(f"✗ Re-authentication failed: {error}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("No existing authentication found. Authenticating...")
|
||||
success, error = auth.authenticate(
|
||||
server_url=server_url,
|
||||
hostname=screen_name,
|
||||
quickconnect_code=quickconnect_key
|
||||
)
|
||||
if success:
|
||||
print(f"✓ Authentication successful!")
|
||||
else:
|
||||
print(f"✗ Authentication failed: {error}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test playlist fetch
|
||||
print("\n" + "=" * 80)
|
||||
print("2. TESTING PLAYLIST FETCH")
|
||||
print("=" * 80)
|
||||
|
||||
playlist_data = auth.get_playlist()
|
||||
|
||||
if playlist_data:
|
||||
print(f"✓ Playlist fetched successfully!")
|
||||
print(f"\nPlaylist Version: {playlist_data.get('playlist_version', 'N/A')}")
|
||||
print(f"Number of items: {len(playlist_data.get('playlist', []))}")
|
||||
|
||||
print("\n" + "-" * 80)
|
||||
print("PLAYLIST ITEMS:")
|
||||
print("-" * 80)
|
||||
|
||||
for idx, item in enumerate(playlist_data.get('playlist', []), 1):
|
||||
print(f"\n{idx}. File: {item.get('file_name', 'N/A')}")
|
||||
print(f" URL: {item.get('url', 'N/A')}")
|
||||
print(f" Duration: {item.get('duration', 'N/A')}s")
|
||||
|
||||
# Check if URL is relative or absolute
|
||||
url = item.get('url', '')
|
||||
if url.startswith('http://') or url.startswith('https://'):
|
||||
print(f" Type: Absolute URL")
|
||||
else:
|
||||
print(f" Type: Relative path (will fail to download!)")
|
||||
|
||||
# Save full response
|
||||
with open('server_response_debug.json', 'w') as f:
|
||||
json.dump(playlist_data, f, indent=2)
|
||||
print(f"\n✓ Full response saved to: server_response_debug.json")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY")
|
||||
print("=" * 80)
|
||||
print(f"Server has: {len(playlist_data.get('playlist', []))} files")
|
||||
print(f"Local has: 3 files (from playlists/server_playlist_v8.json)")
|
||||
|
||||
if len(playlist_data.get('playlist', [])) > 3:
|
||||
print(f"\n⚠️ PROBLEM: Server has {len(playlist_data.get('playlist', []))} files but only 3 were saved!")
|
||||
print("\nMissing files are likely:")
|
||||
local_files = ['music.jpg', '130414-746934884.mp4', 'IMG_0386.jpeg']
|
||||
server_files = [item.get('file_name', '') for item in playlist_data.get('playlist', [])]
|
||||
missing = [f for f in server_files if f not in local_files]
|
||||
for f in missing:
|
||||
print(f" - {f}")
|
||||
|
||||
else:
|
||||
print("✗ Failed to fetch playlist")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Direct API test to check server playlist."""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Try with the saved auth
|
||||
auth_file = 'src/player_auth.json'
|
||||
with open(auth_file, 'r') as f:
|
||||
auth_data = json.load(f)
|
||||
|
||||
server_url = auth_data['server_url']
|
||||
auth_code = auth_data['auth_code']
|
||||
|
||||
print("=" * 80)
|
||||
print("DIRECT API TEST")
|
||||
print("=" * 80)
|
||||
print(f"Server: {server_url}")
|
||||
print(f"Auth code: {auth_code[:20]}...")
|
||||
print()
|
||||
|
||||
# Try to get playlist
|
||||
try:
|
||||
url = f"{server_url}/api/player/playlist"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {auth_code}'
|
||||
}
|
||||
|
||||
print(f"Fetching: {url}")
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
|
||||
print(f"Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"\nPlaylist version: {data.get('playlist_version', 'N/A')}")
|
||||
print(f"Number of items: {len(data.get('playlist', []))}")
|
||||
|
||||
print("\nPlaylist items:")
|
||||
for idx, item in enumerate(data.get('playlist', []), 1):
|
||||
print(f"\n {idx}. {item.get('file_name', 'N/A')}")
|
||||
print(f" URL: {item.get('url', 'N/A')}")
|
||||
print(f" Duration: {item.get('duration', 'N/A')}s")
|
||||
|
||||
# Save full response
|
||||
with open('server_playlist_full.json', 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
print(f"\nFull response saved to: server_playlist_full.json")
|
||||
else:
|
||||
print(f"Error: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the enhanced logging without running the full GUI
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
def simulate_playback_check():
|
||||
"""Simulate the playback logic to see what would happen"""
|
||||
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
media_dir = os.path.join(base_dir, 'media')
|
||||
playlists_dir = os.path.join(base_dir, 'playlists')
|
||||
|
||||
# Supported extensions
|
||||
VIDEO_EXTENSIONS = ['.mp4', '.avi', '.mkv', '.mov', '.webm']
|
||||
IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
|
||||
|
||||
# Load playlist
|
||||
playlist_file = os.path.join(playlists_dir, 'server_playlist_v8.json')
|
||||
with open(playlist_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
playlist = data.get('playlist', [])
|
||||
|
||||
print("=" * 80)
|
||||
print("SIMULATING PLAYBACK SEQUENCE")
|
||||
print("=" * 80)
|
||||
|
||||
for idx, media_item in enumerate(playlist):
|
||||
file_name = media_item.get('file_name', '')
|
||||
duration = media_item.get('duration', 10)
|
||||
|
||||
print(f"\n[STEP {idx + 1}] ===== Playing item {idx + 1}/{len(playlist)} =====")
|
||||
print(f" File: {file_name}")
|
||||
print(f" Duration: {duration}s")
|
||||
|
||||
# Construct path
|
||||
media_path = os.path.join(media_dir, file_name)
|
||||
print(f" Full path: {media_path}")
|
||||
|
||||
# Check existence
|
||||
if not os.path.exists(media_path):
|
||||
print(f" \u274c Media file not found: {media_path}")
|
||||
print(f" ACTION: Skipping to next media...")
|
||||
continue
|
||||
|
||||
file_size = os.path.getsize(media_path)
|
||||
print(f" \u2713 File exists (size: {file_size:,} bytes)")
|
||||
|
||||
# Check extension
|
||||
file_extension = os.path.splitext(file_name)[1].lower()
|
||||
print(f" Extension: {file_extension}")
|
||||
|
||||
if file_extension in VIDEO_EXTENSIONS:
|
||||
print(f" Media type: VIDEO")
|
||||
print(f" ACTION: play_video('{media_path}', {duration})")
|
||||
print(f" - Creating Video widget...")
|
||||
print(f" - Adding to content area...")
|
||||
print(f" - Scheduling next media in {duration}s")
|
||||
print(f" \u2713 Media started successfully")
|
||||
elif file_extension in IMAGE_EXTENSIONS:
|
||||
print(f" Media type: IMAGE")
|
||||
print(f" ACTION: play_image('{media_path}', {duration})")
|
||||
print(f" - Creating AsyncImage widget...")
|
||||
print(f" - Adding to content area...")
|
||||
print(f" - Scheduling next media in {duration}s")
|
||||
print(f" \u2713 Image displayed successfully")
|
||||
else:
|
||||
print(f" \u274c Unsupported media type: {file_extension}")
|
||||
print(f" Supported: .mp4/.avi/.mkv/.mov/.webm/.jpg/.jpeg/.png/.bmp/.gif")
|
||||
print(f" ACTION: Skipping to next media...")
|
||||
continue
|
||||
|
||||
print(f"\n [After {duration}s] Transitioning to next media (was index {idx})")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("END OF PLAYLIST - Would restart from beginning")
|
||||
print("=" * 80)
|
||||
|
||||
if __name__ == '__main__':
|
||||
simulate_playback_check()
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to check what playlist the server is actually returning."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from get_playlists_v2 import fetch_server_playlist
|
||||
|
||||
# Load config
|
||||
config_file = 'config/app_config.json'
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
print("=" * 80)
|
||||
print("TESTING SERVER PLAYLIST FETCH")
|
||||
print("=" * 80)
|
||||
|
||||
# Fetch playlist from server
|
||||
print("\n1. Fetching playlist from server...")
|
||||
server_data = fetch_server_playlist(config)
|
||||
|
||||
print(f"\n2. Server Response:")
|
||||
print(f" Version: {server_data.get('version', 'N/A')}")
|
||||
print(f" Playlist items: {len(server_data.get('playlist', []))}")
|
||||
|
||||
print(f"\n3. Detailed Playlist Items:")
|
||||
for idx, item in enumerate(server_data.get('playlist', []), 1):
|
||||
print(f"\n Item {idx}:")
|
||||
print(f" file_name: {item.get('file_name', 'N/A')}")
|
||||
print(f" url: {item.get('url', 'N/A')}")
|
||||
print(f" duration: {item.get('duration', 'N/A')}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print(f"TOTAL: Server has {len(server_data.get('playlist', []))} files")
|
||||
print("=" * 80)
|
||||
|
||||
# Save to file for inspection
|
||||
output_file = 'server_response_debug.json'
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(server_data, f, indent=2)
|
||||
print(f"\nFull server response saved to: {output_file}")
|
||||
Reference in New Issue
Block a user