Compare commits

...

21 Commits

Author SHA1 Message Date
02e9ea1aaa updated to monitor the netowrk and reset wifi if is not working 2025-12-10 15:53:30 +02:00
Kiwy Signage Player
4c3ddbef73 updated to correctly play the playlist and reload images after edited 2025-12-10 00:09:20 +02:00
Kiwy Signage Player
87e059e0f4 updated to corect function the play pause function 2025-12-09 18:53:34 +02:00
Kiwy Signage Player
46d9fcf6e3 delete watch dog 2025-12-08 21:52:13 +02:00
Kiwy Signage Player
f1a84d05d5 updated buttons in settings 2025-12-08 21:52:03 +02:00
Kiwy Signage Player
706af95557 deleted unnecesary files 2025-12-08 18:17:09 +02:00
02227a12e5 updeated to read specific card 2025-12-08 15:45:37 +02:00
9d32f43ac7 Add edit feature enable/disable setting
- Added checkbox in Settings screen to enable/disable edit feature
- Setting stored in app_config.json as 'edit_feature_enabled'
- Edit workflow now validates: player setting, media type, server permission, card auth
- Shows appropriate error message when edit is blocked at any validation step
- Defaults to enabled (true) if not set
- All conditions must be met for edit interface to open
2025-12-08 14:30:12 +02:00
af1e671c7f Add USB card reader authentication for edit feature
- Implemented CardReader class to read data from USB card readers
- Added CardSwipePopup with 5-second timeout and visual feedback
- Card data is captured and included in edit metadata
- Card data sent to server when edited images are uploaded
- Added evdev dependency for USB input device handling
- Fallback mode when evdev not available (for development)
- Created test utility (test_card_reader.py) for card reader testing
- Added comprehensive documentation (CARD_READER_AUTHENTICATION.md)
- Added access-card.png icon for authentication popup
- Edit interface requires card swipe or times out after 5 seconds
2025-12-08 14:05:04 +02:00
Kiwy Signage Player
9664ad541b moved files 2025-12-06 00:12:37 +02:00
Kiwy Signage Player
89e5ad86dd Add media editing features: WebP support, edit permissions, user auth, server upload
- Migrated to get_playlists_v2 with improved auth system
- Added WebP image format support for playback and editing
- Implemented edit_on_player permission check from server playlist
- Added user authentication layer for edit function (placeholder: player_1)
- Implemented versioned saving with metadata (user, timestamp, version)
- Added server upload functionality for edited media
- Fixed playlist update after intro video completion
- Added hostname and quickconnect_code to player feedback
- Improved error handling for upload failures (non-blocking)
2025-12-06 00:07:48 +02:00
Kiwy Signage Player
f573af0505 update 2025-12-05 00:36:38 +02:00
Kiwy Signage Player
fba2007bdf Improve edit interface: full-screen layout with toolbars, versioned saving to edited_media folder 2025-12-05 00:25:53 +02:00
Kiwy Signage Player
72d382b96b Add image editing feature with drawing tools
- Added pencil edit button to player controls
- Created EditPopup with drawing layer for image annotation
- Drawing tools: color selection (red/blue/green/black), thickness control
- Features: undo last stroke, clear all strokes, save edited image
- Playback automatically pauses during editing
- Only images (.jpg, .jpeg, .png, .bmp) can be edited
- Edited images saved with '_edited' suffix in same directory
- Drawing layer with touch support for annotations
- Full toolbar with color, thickness, and action controls
2025-12-04 22:40:05 +02:00
Kiwy Signage Player
07b7e96edd Add custom on-screen keyboard feature and fix installation
- Added custom half-width on-screen keyboard widget (keyboard_widget.py)
- Keyboard appears at bottom center when input fields are active
- Integrated keyboard in exit popup and settings popup
- Fixed install.sh: added --break-system-packages flag for pip3
- Fixed install.sh: added fallback to online installation for version mismatches
- Removed old Kivy 2.1.0 tar.gz that was causing conflicts
- Keyboard includes close button for intuitive dismissal
- All input fields trigger keyboard on touch
- Keyboard automatically cleans up on popup dismiss
2025-12-04 22:17:57 +02:00
744681bb20 update player with wachdog and intro 2025-11-22 10:26:20 +02:00
68ed5b8534 updated player with playlist 2025-11-22 10:04:30 +02:00
3f9674517d updated player 2025-11-22 09:48:48 +02:00
493f307599 updated to play video 2025-11-21 22:46:29 +02:00
da1d515cc5 updated 2025-11-21 22:20:23 +02:00
4d803d4fe9 updated files 2025-11-21 22:20:06 +02:00
57 changed files with 4603 additions and 149 deletions

1
.player_stop_requested Normal file
View File

@@ -0,0 +1 @@
User requested exit via password

76
check_player_status.sh Executable file
View File

@@ -0,0 +1,76 @@
#!/bin/bash
# Player Status Checker
# Check if the player is running and healthy
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HEARTBEAT_FILE="$SCRIPT_DIR/.player_heartbeat"
STOP_FLAG_FILE="$SCRIPT_DIR/.player_stop_requested"
LOG_FILE="$SCRIPT_DIR/player_watchdog.log"
echo "=========================================="
echo "Kivy Signage Player - Status Check"
echo "=========================================="
echo ""
# Check for stop flag
if [ -f "$STOP_FLAG_FILE" ]; then
echo "🛑 Stop Flag: PRESENT (user requested exit)"
echo " Watchdog will not restart player"
echo " To restart: ./start.sh"
echo ""
fi
# Check if player process is running
PLAYER_PID=$(pgrep -f "python3 main.py" | head -1)
if [ -z "$PLAYER_PID" ]; then
echo "Status: ❌ NOT RUNNING"
echo ""
echo "Player process is not running"
else
echo "Status: ✓ RUNNING"
echo "PID: $PLAYER_PID"
echo ""
# Check heartbeat
if [ -f "$HEARTBEAT_FILE" ]; then
last_update=$(stat -c %Y "$HEARTBEAT_FILE" 2>/dev/null || echo 0)
current_time=$(date +%s)
diff=$((current_time - last_update))
echo "Heartbeat: $(date -d @${last_update} '+%Y-%m-%d %H:%M:%S')"
echo "Last update: ${diff}s ago"
if [ $diff -lt 60 ]; then
echo "Health: ✓ HEALTHY"
else
echo "Health: ⚠️ STALE (may be frozen)"
fi
else
echo "Heartbeat: Not found (player may be starting)"
fi
fi
echo ""
# Show watchdog status
if pgrep -f "start.sh" > /dev/null; then
echo "Watchdog: ✓ ACTIVE"
else
echo "Watchdog: ❌ NOT RUNNING"
echo ""
echo "To start with watchdog: ./start.sh"
fi
echo ""
# Show last few log entries
if [ -f "$LOG_FILE" ]; then
echo "=========================================="
echo "Recent Log Entries (last 10):"
echo "=========================================="
tail -10 "$LOG_FILE"
fi
echo ""

View File

@@ -1,9 +1,10 @@
{
"server_ip": "172.18.0.1",
"port": "5000",
"screen_name": "rpi-tvholba1",
"server_ip": "digi-signage.moto-adv.com",
"port": "443",
"screen_name": "tv-terasa",
"quickconnect_key": "8887779",
"orientation": "Landscape",
"touch": "True",
"max_resolution": "1920x1080"
"max_resolution": "1920x1080",
"edit_feature_enabled": true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
config/resources/intro1.mp4 Normal file

Binary file not shown.

BIN
config/resources/pencil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -83,13 +83,18 @@ if [ "$OFFLINE_MODE" = true ] && [ -d "$WHEELS_DIR" ] && [ "$(ls -A $WHEELS_DIR/
echo "Installing from offline Python wheels..."
echo "Wheel files found: $(ls -1 $WHEELS_DIR/*.whl 2>/dev/null | wc -l)"
pip3 install --no-index --find-links="$WHEELS_DIR" -r requirements.txt
echo "Python packages installed from offline repository"
if pip3 install --break-system-packages --no-index --find-links="$WHEELS_DIR" -r requirements.txt 2>&1 | tee /tmp/pip_install.log; then
echo "Python packages installed from offline repository"
else
echo "Warning: Offline installation failed (possibly due to Python version mismatch)"
echo "Falling back to online installation..."
pip3 install --break-system-packages -r requirements.txt
echo "Python packages installed from PyPI"
fi
else
# Online: Use pip from PyPI
echo "Installing from PyPI..."
pip3 install -r requirements.txt
pip3 install --break-system-packages -r requirements.txt
echo "Python packages installed successfully"
fi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,7 @@
kivy==2.1.0
kivy>=2.3.0
ffpyplayer
requests==2.32.4
bcrypt==4.2.1
aiohttp==3.9.1
asyncio==3.4.3
asyncio==3.4.3
evdev>=1.6.0

53
setup_wifi_control.sh Normal file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
# Setup script to allow passwordless sudo for WiFi control commands
echo "Setting up passwordless sudo for WiFi control..."
echo ""
# Create sudoers file for WiFi commands
SUDOERS_FILE="/etc/sudoers.d/kiwy-signage-wifi"
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "This script must be run as root (use sudo)"
echo "Usage: sudo bash setup_wifi_control.sh"
exit 1
fi
# Get the username who invoked sudo
ACTUAL_USER="${SUDO_USER:-$USER}"
echo "Configuring passwordless sudo for user: $ACTUAL_USER"
echo ""
# Create sudoers entry
cat > "$SUDOERS_FILE" << EOF
# Allow $ACTUAL_USER to control WiFi without password for Kiwy Signage Player
$ACTUAL_USER ALL=(ALL) NOPASSWD: /usr/sbin/rfkill block wifi
$ACTUAL_USER ALL=(ALL) NOPASSWD: /usr/sbin/rfkill unblock wifi
$ACTUAL_USER ALL=(ALL) NOPASSWD: /sbin/ifconfig wlan0 down
$ACTUAL_USER ALL=(ALL) NOPASSWD: /sbin/ifconfig wlan0 up
$ACTUAL_USER ALL=(ALL) NOPASSWD: /sbin/dhclient wlan0
EOF
# Set correct permissions
chmod 0440 "$SUDOERS_FILE"
echo "✓ Created sudoers file: $SUDOERS_FILE"
echo ""
# Validate the sudoers file
if visudo -c -f "$SUDOERS_FILE"; then
echo "✓ Sudoers file validated successfully"
echo ""
echo "Setup complete! User '$ACTUAL_USER' can now control WiFi without password."
echo ""
echo "Test with:"
echo " sudo rfkill block wifi"
echo " sudo rfkill unblock wifi"
else
echo "✗ Error: Sudoers file validation failed"
echo "Removing invalid file..."
rm -f "$SUDOERS_FILE"
exit 1
fi

100
src/edit_drowing.py Normal file
View File

@@ -0,0 +1,100 @@
Kiwy drawing
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.image import Image
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.graphics import Color, Line
from kivy.core.window import Window
class DrawLayer(Widget):
def init(self, **kwargs):
super().init(**kwargs)
self.strokes = [] # store all drawn lines
self.current_color = (1, 0, 0) # default red
self.current_width = 2 # default thickness
self.drawing_enabled = False # drawing toggle
def on_touch_down(self, touch):
if not self.drawing_enabled:
return False
with self.canvas:
Color(*self.current_color)
new_line = Line(points=[touch.x, touch.y], width=self.current_width)
self.strokes.append(new_line)
return True
def on_touch_move(self, touch):
if self.strokes and self.drawing_enabled:
self.strokes[-1].points += [touch.x, touch.y]
# ==========================
# UNDO LAST LINE
# ==========================
def undo(self):
if self.strokes:
last = self.strokes.pop()
self.canvas.remove(last)
# ==========================
# CHANGE COLOR
# ==========================
def set_color(self, color_tuple):
self.current_color = color_tuple
# ==========================
# CHANGE LINE WIDTH
# ==========================
def set_thickness(self, value):
self.current_width = value
class EditorUI(BoxLayout):
def init(self, **kwargs):
super().init(orientation="vertical", **kwargs)
# Background image
self.img = Image(source="graph.png", allow_stretch=True)
self.add_widget(self.img)
# Drawing layer above image
self.draw = DrawLayer()
self.add_widget(self.draw)
# Toolbar
toolbar = BoxLayout(size_hint_y=0.15)
toolbar.add_widget(Button(text="Draw On", on_press=self.toggle_draw))
toolbar.add_widget(Button(text="Red", on_press=lambda x: self.draw.set_color((1,0,0))))
toolbar.add_widget(Button(text="Blue", on_press=lambda x: self.draw.set_color((0,0,1))))
toolbar.add_widget(Button(text="Green", on_press=lambda x: self.draw.set_color((0,1,0))))
toolbar.add_widget(Button(text="Thin", on_press=lambda x: self.draw.set_thickness(2)))
toolbar.add_widget(Button(text="Thick", on_press=lambda x: self.draw.set_thickness(6)))
toolbar.add_widget(Button(text="Undo", on_press=lambda x: self.draw.undo()))
toolbar.add_widget(Button(text="Save", on_press=lambda x: self.save_image()))
self.add_widget(toolbar)
# ==========================
# TOGGLE DRAWING MODE
# ==========================
def toggle_draw(self, btn):
self.draw.drawing_enabled = not self.draw.drawing_enabled
btn.text = "Draw Off" if self.draw.drawing_enabled else "Draw On"
# ==========================
# SAVE MERGED IMAGE
# ==========================
def save_image(self):
self.export_to_png("edited_graph.png")
print("Saved as edited_graph.png")
class AnnotatorApp(App):
def build(self):
return EditorUI()
AnnotatorApp().run()

View File

@@ -237,7 +237,8 @@ def download_media_files(playlist, media_dir):
updated_media = {
'file_name': file_name,
'url': os.path.relpath(local_path, os.path.dirname(media_dir)),
'duration': duration
'duration': duration,
'edit_on_player': media.get('edit_on_player', False) # Preserve edit_on_player flag
}
updated_playlist.append(updated_media)
@@ -270,8 +271,53 @@ def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, kee
os.remove(filepath)
logger.info(f"🗑️ Deleted old playlist: {f}")
# TODO: Clean up unused media files
logger.info(f" Cleanup complete (kept {keep_versions} latest versions)")
# Clean up unused media files
logger.info("🔍 Checking for unused media files...")
# Get list of media files referenced in current playlist
current_playlist_file = os.path.join(playlist_dir, f'server_playlist_v{current_version}.json')
referenced_files = set()
if os.path.exists(current_playlist_file):
try:
with open(current_playlist_file, 'r') as f:
playlist_data = json.load(f)
for item in playlist_data.get('playlist', []):
file_name = item.get('file_name', '')
if file_name:
referenced_files.add(file_name)
logger.info(f"📋 Current playlist references {len(referenced_files)} media files")
# Get all files in media directory (excluding edited_media subfolder)
if os.path.exists(media_dir):
media_files = [f for f in os.listdir(media_dir)
if os.path.isfile(os.path.join(media_dir, f))]
deleted_count = 0
for media_file in media_files:
# Skip if file is in current playlist
if media_file in referenced_files:
continue
# Delete unreferenced file
media_path = os.path.join(media_dir, media_file)
try:
os.remove(media_path)
logger.info(f"🗑️ Deleted unused media: {media_file}")
deleted_count += 1
except Exception as e:
logger.warning(f"⚠️ Could not delete {media_file}: {e}")
if deleted_count > 0:
logger.info(f"✅ Deleted {deleted_count} unused media files")
else:
logger.info("✅ No unused media files to delete")
except Exception as e:
logger.error(f"❌ Error reading playlist for media cleanup: {e}")
logger.info(f"✅ Cleanup complete (kept {keep_versions} latest playlist versions)")
except Exception as e:
logger.error(f"❌ Error during cleanup: {e}")

160
src/keyboard_widget.py Normal file
View File

@@ -0,0 +1,160 @@
"""
Custom Keyboard Widget for Signage Player
Provides an on-screen keyboard for text input
"""
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.animation import Animation
from kivy.logger import Logger
from kivy.core.window import Window
from kivy.graphics import Color, RoundedRectangle
class KeyboardWidget(FloatLayout):
"""Custom on-screen keyboard widget"""
def __init__(self, **kwargs):
super(KeyboardWidget, self).__init__(**kwargs)
self.target_input = None
self.size_hint = (None, None)
# Calculate size - half screen width
self.width = Window.width * 0.5
self.height = self.width / 3
# Position at bottom center
self.x = (Window.width - self.width) / 2
self.y = 0
# Start hidden
self.opacity = 0
# Create the keyboard UI
self._build_keyboard()
# Bind window resize
Window.bind(on_resize=self._on_window_resize)
Logger.info(f"KeyboardWidget: Initialized at ({self.x}, {self.y}) with size {self.width}x{self.height}")
def _build_keyboard(self):
"""Build the keyboard UI"""
# Background
with self.canvas.before:
Color(0.1, 0.1, 0.1, 0.95)
self.bg_rect = RoundedRectangle(pos=self.pos, size=self.size, radius=[15, 15, 0, 0])
self.bind(pos=self._update_bg, size=self._update_bg)
# Main layout
main_layout = BoxLayout(orientation='vertical', padding=5, spacing=5)
main_layout.size = self.size
main_layout.pos = self.pos
# Close button bar
close_bar = BoxLayout(orientation='horizontal', size_hint=(1, None), height=40, padding=[5, 0])
close_bar.add_widget(Widget())
close_btn = Button(text='', size_hint=(None, 1), width=40,
background_color=(0.8, 0.2, 0.2, 0.9), font_size='20sp', bold=True)
close_btn.bind(on_press=lambda x: self.hide_keyboard())
close_bar.add_widget(close_btn)
main_layout.add_widget(close_bar)
# Number row
self._add_key_row(main_layout, ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'])
# Top letter row
self._add_key_row(main_layout, ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'])
# Middle letter row (with offset)
middle_row = BoxLayout(size_hint=(1, 1), spacing=3)
middle_row.add_widget(Widget(size_hint=(0.5, 1)))
for letter in ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L']:
btn = Button(text=letter)
btn.bind(on_press=lambda x, l=letter: self.key_pressed(l.lower()))
middle_row.add_widget(btn)
middle_row.add_widget(Widget(size_hint=(0.5, 1)))
main_layout.add_widget(middle_row)
# Bottom letter row (with offset)
bottom_row = BoxLayout(size_hint=(1, 1), spacing=3)
bottom_row.add_widget(Widget(size_hint=(1, 1)))
for letter in ['Z', 'X', 'C', 'V', 'B', 'N', 'M']:
btn = Button(text=letter)
btn.bind(on_press=lambda x, l=letter: self.key_pressed(l.lower()))
bottom_row.add_widget(btn)
bottom_row.add_widget(Widget(size_hint=(1, 1)))
main_layout.add_widget(bottom_row)
# Space and backspace row
last_row = BoxLayout(size_hint=(1, 1), spacing=3)
backspace_btn = Button(text='', size_hint=(0.3, 1), font_size='24sp')
backspace_btn.bind(on_press=lambda x: self.key_pressed('backspace'))
last_row.add_widget(backspace_btn)
space_btn = Button(text='Space', size_hint=(0.7, 1))
space_btn.bind(on_press=lambda x: self.key_pressed(' '))
last_row.add_widget(space_btn)
main_layout.add_widget(last_row)
self.add_widget(main_layout)
def _add_key_row(self, parent, keys):
"""Add a row of keys"""
row = BoxLayout(size_hint=(1, 1), spacing=3)
for key in keys:
btn = Button(text=key)
btn.bind(on_press=lambda x, k=key: self.key_pressed(k.lower() if k.isalpha() else k))
row.add_widget(btn)
parent.add_widget(row)
def _update_bg(self, *args):
"""Update background rectangle"""
self.bg_rect.pos = self.pos
self.bg_rect.size = self.size
def _on_window_resize(self, window, width, height):
"""Handle window resize"""
self.width = width * 0.5
self.height = self.width / 3
self.x = (width - self.width) / 2
self.y = 0
def show_keyboard(self, target_input):
"""Show the keyboard for a specific TextInput"""
self.target_input = target_input
Logger.info(f"KeyboardWidget: Showing keyboard for {target_input}")
# Animate keyboard appearing
anim = Animation(opacity=1, duration=0.2)
anim.start(self)
def hide_keyboard(self):
"""Hide the keyboard"""
Logger.info("KeyboardWidget: Hiding keyboard")
# Animate keyboard disappearing
anim = Animation(opacity=0, duration=0.2)
anim.start(self)
# Clear target
if self.target_input:
self.target_input.focus = False
self.target_input = None
def key_pressed(self, key):
"""Handle key press"""
if not self.target_input:
return
if key == 'backspace':
# Remove last character
if self.target_input.text:
self.target_input.text = self.target_input.text[:-1]
else:
# Add character
self.target_input.text += key
Logger.debug(f"KeyboardWidget: Key pressed '{key}', current text: {self.target_input.text}")

File diff suppressed because it is too large Load Diff

235
src/network_monitor.py Normal file
View File

@@ -0,0 +1,235 @@
"""
Network Monitoring Module
Checks server connectivity and manages WiFi restart on connection failure
"""
import subprocess
import time
import random
import requests
from datetime import datetime
from kivy.logger import Logger
from kivy.clock import Clock
class NetworkMonitor:
"""Monitor network connectivity and manage WiFi restart"""
def __init__(self, server_url, check_interval_min=30, check_interval_max=45, wifi_restart_duration=20):
"""
Initialize network monitor
Args:
server_url (str): Server URL to check connectivity (e.g., 'https://digi-signage.moto-adv.com')
check_interval_min (int): Minimum minutes between checks (default: 30)
check_interval_max (int): Maximum minutes between checks (default: 45)
wifi_restart_duration (int): Minutes to keep WiFi off during restart (default: 20)
"""
self.server_url = server_url.rstrip('/')
self.check_interval_min = check_interval_min * 60 # Convert to seconds
self.check_interval_max = check_interval_max * 60 # Convert to seconds
self.wifi_restart_duration = wifi_restart_duration * 60 # Convert to seconds
self.is_monitoring = False
self.scheduled_event = None
self.consecutive_failures = 0
self.max_failures_before_restart = 3 # Restart WiFi after 3 consecutive failures
def start_monitoring(self):
"""Start the network monitoring loop"""
if not self.is_monitoring:
self.is_monitoring = True
Logger.info("NetworkMonitor: Starting network monitoring")
self._schedule_next_check()
def stop_monitoring(self):
"""Stop the network monitoring"""
self.is_monitoring = False
if self.scheduled_event:
self.scheduled_event.cancel()
self.scheduled_event = None
Logger.info("NetworkMonitor: Stopped network monitoring")
def _schedule_next_check(self):
"""Schedule the next connectivity check at a random interval"""
if not self.is_monitoring:
return
# Random interval between min and max
next_check_seconds = random.randint(self.check_interval_min, self.check_interval_max)
next_check_minutes = next_check_seconds / 60
Logger.info(f"NetworkMonitor: Next check scheduled in {next_check_minutes:.1f} minutes")
# Schedule using Kivy Clock
self.scheduled_event = Clock.schedule_once(
lambda dt: self._check_connectivity(),
next_check_seconds
)
def _check_connectivity(self):
"""Check network connectivity to server"""
Logger.info("NetworkMonitor: Checking server connectivity...")
if self._test_server_connection():
Logger.info("NetworkMonitor: ✓ Server connection successful")
self.consecutive_failures = 0
else:
self.consecutive_failures += 1
Logger.warning(f"NetworkMonitor: ✗ Server connection failed (attempt {self.consecutive_failures}/{self.max_failures_before_restart})")
if self.consecutive_failures >= self.max_failures_before_restart:
Logger.error("NetworkMonitor: Multiple connection failures detected - initiating WiFi restart")
self._restart_wifi()
self.consecutive_failures = 0 # Reset counter after restart
# Schedule next check
self._schedule_next_check()
def _test_server_connection(self):
"""
Test connection to the server using ping only
This works in closed networks where the server is local
Returns:
bool: True if server is reachable, False otherwise
"""
try:
# Extract hostname from server URL (remove http:// or https://)
hostname = self.server_url.replace('https://', '').replace('http://', '').split('/')[0]
Logger.info(f"NetworkMonitor: Pinging server: {hostname}")
# Ping the server hostname with 3 attempts
result = subprocess.run(
['ping', '-c', '3', '-W', '3', hostname],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
Logger.info(f"NetworkMonitor: ✓ Server {hostname} is reachable")
return True
else:
Logger.warning(f"NetworkMonitor: ✗ Cannot reach server {hostname}")
return False
except subprocess.TimeoutExpired:
Logger.warning(f"NetworkMonitor: ✗ Ping timeout to server")
return False
except Exception as e:
Logger.error(f"NetworkMonitor: Error pinging server: {e}")
return False
def _restart_wifi(self):
"""
Restart WiFi by turning it off for a specified duration then back on
This runs in a separate thread to not block the main application
"""
def wifi_restart_thread():
try:
Logger.info("NetworkMonitor: ====================================")
Logger.info("NetworkMonitor: INITIATING WIFI RESTART SEQUENCE")
Logger.info("NetworkMonitor: ====================================")
# Turn off WiFi using rfkill (more reliable on Raspberry Pi)
Logger.info("NetworkMonitor: Turning WiFi OFF using rfkill...")
result = subprocess.run(
['sudo', 'rfkill', 'block', 'wifi'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
Logger.info("NetworkMonitor: ✓ WiFi turned OFF successfully (rfkill)")
Logger.info("NetworkMonitor: WiFi is now DISABLED and will remain OFF")
else:
Logger.error(f"NetworkMonitor: rfkill failed, trying ifconfig...")
Logger.error(f"NetworkMonitor: rfkill error: {result.stderr}")
# Fallback to ifconfig
result2 = subprocess.run(
['sudo', 'ifconfig', 'wlan0', 'down'],
capture_output=True,
text=True,
timeout=10
)
if result2.returncode == 0:
Logger.info("NetworkMonitor: ✓ WiFi turned OFF successfully (ifconfig)")
else:
Logger.error(f"NetworkMonitor: Failed to turn WiFi off: {result2.stderr}")
Logger.error(f"NetworkMonitor: Return code: {result2.returncode}")
Logger.error(f"NetworkMonitor: STDOUT: {result2.stdout}")
return
# Wait for the specified duration with WiFi OFF
wait_minutes = self.wifi_restart_duration / 60
Logger.info(f"NetworkMonitor: ====================================")
Logger.info(f"NetworkMonitor: WiFi will remain OFF for {wait_minutes:.0f} minutes")
Logger.info(f"NetworkMonitor: Waiting period started at: {datetime.now().strftime('%H:%M:%S')}")
Logger.info(f"NetworkMonitor: ====================================")
# Sleep while WiFi is OFF
time.sleep(self.wifi_restart_duration)
Logger.info(f"NetworkMonitor: Wait period completed at: {datetime.now().strftime('%H:%M:%S')}")
# Turn WiFi back on after the wait period
Logger.info("NetworkMonitor: ====================================")
Logger.info("NetworkMonitor: Now turning WiFi back ON...")
Logger.info("NetworkMonitor: ====================================")
# Unblock WiFi using rfkill
result = subprocess.run(
['sudo', 'rfkill', 'unblock', 'wifi'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
Logger.info("NetworkMonitor: ✓ WiFi unblocked successfully (rfkill)")
else:
Logger.error(f"NetworkMonitor: rfkill unblock failed: {result.stderr}")
# Also bring interface up
result2 = subprocess.run(
['sudo', 'ifconfig', 'wlan0', 'up'],
capture_output=True,
text=True,
timeout=10
)
if result2.returncode == 0:
Logger.info("NetworkMonitor: ✓ WiFi interface brought UP successfully")
# Wait a bit for connection to establish
Logger.info("NetworkMonitor: Waiting 10 seconds for WiFi to initialize...")
time.sleep(10)
# Try to restart DHCP
Logger.info("NetworkMonitor: Requesting IP address...")
subprocess.run(
['sudo', 'dhclient', 'wlan0'],
capture_output=True,
text=True,
timeout=15
)
Logger.info("NetworkMonitor: ====================================")
Logger.info("NetworkMonitor: WIFI RESTART SEQUENCE COMPLETED")
Logger.info("NetworkMonitor: ====================================")
else:
Logger.error(f"NetworkMonitor: Failed to turn WiFi on: {result.stderr}")
except subprocess.TimeoutExpired:
Logger.error("NetworkMonitor: WiFi restart command timeout")
except Exception as e:
Logger.error(f"NetworkMonitor: Error during WiFi restart: {e}")
# Run in separate thread to not block the application
import threading
thread = threading.Thread(target=wifi_restart_thread, daemon=True)
thread.start()

View File

@@ -1,10 +1,10 @@
{
"hostname": "rpi-tvholba1",
"auth_code": "aDHIMS2yx_HhfR0dWKy9VHaM_h0CKemfcsqv4Zgp0IY",
"player_id": 2,
"player_name": "Tv-Anunturi",
"group_id": null,
"hostname": "tv-terasa",
"auth_code": "vkrxEO6eOTxkzXJBtoN4OuXc8eaX2mC3AB9ZePrnick",
"player_id": 1,
"player_name": "TV-acasa",
"playlist_id": 1,
"orientation": "Landscape",
"authenticated": true,
"server_url": "http://172.18.0.1:5000"
"server_url": "http://digi-signage.moto-adv.com"
}

View File

@@ -275,6 +275,8 @@ class PlayerAuth:
feedback_url = f"{server_url}/api/player-feedback"
headers = {'Authorization': f'Bearer {auth_code}'}
payload = {
'hostname': self.auth_data.get('hostname'),
'quickconnect_code': self.auth_data.get('quickconnect_code'),
'message': message,
'status': status,
'playlist_version': playlist_version,

View File

@@ -1,5 +1,193 @@
#:kivy 2.1.0
# Custom On-Screen Keyboard Widget
<CustomKeyboard@FloatLayout>:
size_hint: None, None
width: root.parent.width * 0.5 if root.parent else dp(600)
height: self.width / 3
pos: (root.parent.width - self.width) / 2 if root.parent else 0, 0
opacity: 0
canvas.before:
Color:
rgba: 0.1, 0.1, 0.1, 0.95
RoundedRectangle:
size: self.size
pos: self.pos
radius: [dp(15), dp(15), 0, 0]
BoxLayout:
orientation: 'vertical'
padding: dp(5)
spacing: dp(5)
# Close button bar
BoxLayout:
size_hint: 1, None
height: dp(40)
padding: [dp(5), 0]
Widget:
Button:
text: '✕'
size_hint: None, 1
width: dp(40)
background_color: 0.8, 0.2, 0.2, 0.9
font_size: sp(20)
bold: True
on_press: root.parent.hide_keyboard() if root.parent and hasattr(root.parent, 'hide_keyboard') else None
# Number row
BoxLayout:
size_hint: 1, 1
spacing: dp(3)
Button:
text: '1'
on_press: root.parent.key_pressed('1') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: '2'
on_press: root.parent.key_pressed('2') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: '3'
on_press: root.parent.key_pressed('3') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: '4'
on_press: root.parent.key_pressed('4') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: '5'
on_press: root.parent.key_pressed('5') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: '6'
on_press: root.parent.key_pressed('6') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: '7'
on_press: root.parent.key_pressed('7') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: '8'
on_press: root.parent.key_pressed('8') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: '9'
on_press: root.parent.key_pressed('9') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: '0'
on_press: root.parent.key_pressed('0') if root.parent and hasattr(root.parent, 'key_pressed') else None
# Top letter row (QWERTYUIOP)
BoxLayout:
size_hint: 1, 1
spacing: dp(3)
Button:
text: 'Q'
on_press: root.parent.key_pressed('q') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'W'
on_press: root.parent.key_pressed('w') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'E'
on_press: root.parent.key_pressed('e') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'R'
on_press: root.parent.key_pressed('r') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'T'
on_press: root.parent.key_pressed('t') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'Y'
on_press: root.parent.key_pressed('y') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'U'
on_press: root.parent.key_pressed('u') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'I'
on_press: root.parent.key_pressed('i') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'O'
on_press: root.parent.key_pressed('o') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'P'
on_press: root.parent.key_pressed('p') if root.parent and hasattr(root.parent, 'key_pressed') else None
# Middle letter row (ASDFGHJKL)
BoxLayout:
size_hint: 1, 1
spacing: dp(3)
Widget:
size_hint: 0.5, 1
Button:
text: 'A'
on_press: root.parent.key_pressed('a') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'S'
on_press: root.parent.key_pressed('s') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'D'
on_press: root.parent.key_pressed('d') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'F'
on_press: root.parent.key_pressed('f') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'G'
on_press: root.parent.key_pressed('g') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'H'
on_press: root.parent.key_pressed('h') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'J'
on_press: root.parent.key_pressed('j') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'K'
on_press: root.parent.key_pressed('k') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'L'
on_press: root.parent.key_pressed('l') if root.parent and hasattr(root.parent, 'key_pressed') else None
Widget:
size_hint: 0.5, 1
# Bottom letter row (ZXCVBNM)
BoxLayout:
size_hint: 1, 1
spacing: dp(3)
Widget:
size_hint: 1, 1
Button:
text: 'Z'
on_press: root.parent.key_pressed('z') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'X'
on_press: root.parent.key_pressed('x') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'C'
on_press: root.parent.key_pressed('c') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'V'
on_press: root.parent.key_pressed('v') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'B'
on_press: root.parent.key_pressed('b') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'N'
on_press: root.parent.key_pressed('n') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'M'
on_press: root.parent.key_pressed('m') if root.parent and hasattr(root.parent, 'key_pressed') else None
Widget:
size_hint: 1, 1
# Bottom row (Space, Backspace)
BoxLayout:
size_hint: 1, 1
spacing: dp(3)
Button:
text: '←'
size_hint: 0.3, 1
font_size: sp(24)
on_press: root.parent.key_pressed('backspace') if root.parent and hasattr(root.parent, 'key_pressed') else None
Button:
text: 'Space'
size_hint: 0.7, 1
on_press: root.parent.key_pressed(' ') if root.parent and hasattr(root.parent, 'key_pressed') else None
<SignagePlayer@FloatLayout>:
size: root.screen_width, root.screen_height
canvas.before:
@@ -36,12 +224,12 @@
pos: self.pos
radius: [dp(15)]
# New control panel overlay (bottom center, 1/6 width, 90% transparent)
# New control panel overlay (bottom center, width for 6 buttons, 90% transparent)
BoxLayout:
id: controls_layout
orientation: 'horizontal'
size_hint: None, None
width: root.width / 6 if root.width > 0 else dp(260)
width: dp(370)
height: dp(70)
pos: (root.width - self.width) / 2, dp(10)
opacity: 1
@@ -68,11 +256,19 @@
id: play_pause_btn
size_hint: None, None
size: dp(50), dp(50)
background_normal: root.resources_path + '/play.png'
background_normal: root.resources_path + '/pause.png'
background_down: root.resources_path + '/pause.png'
border: (0, 0, 0, 0)
on_press: root.toggle_pause()
Button:
id: edit_btn
size_hint: None, None
size: dp(50), dp(50)
background_normal: root.resources_path + '/pencil.png'
background_down: root.resources_path + '/pencil.png'
border: (0, 0, 0, 0)
on_press: root.show_edit_interface()
Button:
id: settings_btn
size_hint: None, None
@@ -126,6 +322,9 @@
font_size: sp(16)
size_hint_y: None
height: dp(40)
write_tab: False
readonly: True
on_focus: root.on_input_focus(self, self.focus)
Label:
id: error_label
@@ -180,6 +379,8 @@
size_hint_x: 0.7
multiline: False
font_size: sp(14)
write_tab: False
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
# Screen name
BoxLayout:
@@ -200,6 +401,8 @@
size_hint_x: 0.7
multiline: False
font_size: sp(14)
write_tab: False
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
# Quickconnect key
BoxLayout:
@@ -220,6 +423,8 @@
size_hint_x: 0.7
multiline: False
font_size: sp(14)
write_tab: False
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
# Orientation
BoxLayout:
@@ -240,6 +445,8 @@
size_hint_x: 0.7
multiline: False
font_size: sp(14)
write_tab: False
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
# Touch
BoxLayout:
@@ -260,6 +467,8 @@
size_hint_x: 0.7
multiline: False
font_size: sp(14)
write_tab: False
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
# Resolution
BoxLayout:
@@ -281,40 +490,134 @@
multiline: False
font_size: sp(14)
hint_text: '1920x1080 or auto'
write_tab: False
on_touch_down: root.on_input_touch(self, args[1]) if self.collide_point(*args[1].pos) else None
# Edit Feature Enable/Disable
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(40)
spacing: dp(10)
Label:
text: 'Enable Edit Feature:'
size_hint_x: 0.3
text_size: self.size
halign: 'left'
valign: 'middle'
CheckBox:
id: edit_enabled_checkbox
size_hint_x: None
width: dp(40)
active: True
on_active: root.on_edit_feature_toggle(self.active)
Label:
text: '(Allow editing images on this player)'
size_hint_x: 0.4
font_size: sp(12)
text_size: self.size
halign: 'left'
valign: 'middle'
color: 0.7, 0.7, 0.7, 1
Widget:
size_hint_y: 0.1
size_hint_y: 0.05
# Status information
# Reset Buttons Section
Label:
id: playlist_info
text: 'Playlist Version: N/A'
text: 'Reset Options:'
size_hint_y: None
height: dp(30)
text_size: self.size
halign: 'left'
valign: 'middle'
bold: True
font_size: sp(16)
# Reset Buttons Row
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(50)
spacing: dp(10)
Label:
id: media_count_info
text: 'Media Count: 0'
size_hint_y: None
height: dp(30)
text_size: self.size
halign: 'left'
valign: 'middle'
Button:
id: reset_auth_btn
text: 'Reset Player Auth'
background_color: 0.8, 0.4, 0.2, 1
on_press: root.reset_player_auth()
Label:
id: status_info
text: 'Status: Idle'
Button:
id: reset_playlist_btn
text: 'Reset Playlist to v0'
background_color: 0.8, 0.4, 0.2, 1
on_press: root.reset_playlist_version()
Button:
id: restart_player_btn
text: 'Restart Player'
background_color: 0.2, 0.6, 0.8, 1
on_press: root.restart_player()
# Test Connection Button
Button:
id: test_connection_btn
text: 'Test Server Connection'
size_hint_y: None
height: dp(30)
height: dp(50)
background_color: 0.2, 0.4, 0.8, 1
on_press: root.test_connection()
# Connection Status Label
Label:
id: connection_status
text: 'Click button to test connection'
size_hint_y: None
height: dp(40)
text_size: self.size
halign: 'left'
halign: 'center'
valign: 'middle'
color: 0.7, 0.7, 0.7, 1
Widget:
size_hint_y: 0.2
size_hint_y: 0.05
# Status information row
BoxLayout:
orientation: 'horizontal'
size_hint_y: None
height: dp(30)
spacing: dp(10)
Label:
id: playlist_info
text: 'Playlist: N/A'
text_size: self.size
halign: 'center'
valign: 'middle'
font_size: sp(12)
Label:
id: media_count_info
text: 'Media: 0'
text_size: self.size
halign: 'center'
valign: 'middle'
font_size: sp(12)
Label:
id: status_info
text: 'Status: Idle'
text_size: self.size
halign: 'center'
valign: 'middle'
font_size: sp(12)
Widget:
size_hint_y: 0.05
# Action buttons
BoxLayout:

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
Test script for network monitor functionality
"""
import sys
import time
from kivy.app import App
from kivy.clock import Clock
from network_monitor import NetworkMonitor
class TestMonitorApp(App):
"""Minimal Kivy app to test network monitor"""
def build(self):
"""Build the app"""
from kivy.uix.label import Label
return Label(text='Network Monitor Test Running\nCheck terminal for output')
def on_start(self):
"""Start monitoring when app starts"""
server_url = "https://digi-signage.moto-adv.com"
print("=" * 60)
print("Network Monitor Test")
print("=" * 60)
print()
print(f"Server URL: {server_url}")
print("Check interval: 0.5 minutes (30 seconds for testing)")
print("WiFi restart duration: 1 minute (for testing)")
print()
# Create monitor with short intervals for testing
self.monitor = NetworkMonitor(
server_url=server_url,
check_interval_min=0.5, # 30 seconds
check_interval_max=0.5, # 30 seconds
wifi_restart_duration=1 # 1 minute
)
# Perform immediate test
print("Performing immediate connectivity test...")
self.monitor._check_connectivity()
# Start monitoring for future checks
print("\nStarting periodic network monitoring...")
self.monitor.start_monitoring()
print("\nMonitoring is active. Press Ctrl+C to stop.")
print("Next check will occur in ~30 seconds.")
print()
def on_stop(self):
"""Stop monitoring when app stops"""
if hasattr(self, 'monitor'):
self.monitor.stop_monitoring()
print("\nNetwork monitoring stopped")
print("Test completed!")
if __name__ == '__main__':
TestMonitorApp().run()

160
start.sh
View File

@@ -1,58 +1,152 @@
#!/bin/bash
# Kivy Signage Player Startup Script
# This script activates the virtual environment and starts the player
# Kivy Signage Player Startup Script with Watchdog
# This script monitors and auto-restarts the player if it crashes
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "=========================================="
echo "Starting Kivy Signage Player"
echo "=========================================="
echo ""
# Configuration
MAX_RETRIES=999999 # Effectively unlimited retries
RESTART_DELAY=5 # Seconds to wait before restart
HEALTH_CHECK_INTERVAL=30 # Seconds between health checks
HEARTBEAT_FILE="$SCRIPT_DIR/.player_heartbeat"
STOP_FLAG_FILE="$SCRIPT_DIR/.player_stop_requested"
LOG_FILE="$SCRIPT_DIR/player_watchdog.log"
# Function to log messages
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# Function to check if player is healthy
check_health() {
# Check if heartbeat file exists and is recent (within last 60 seconds)
if [ -f "$HEARTBEAT_FILE" ]; then
local last_update=$(stat -c %Y "$HEARTBEAT_FILE" 2>/dev/null || echo 0)
local current_time=$(date +%s)
local diff=$((current_time - last_update))
if [ $diff -lt 60 ]; then
return 0 # Healthy
else
log_message "⚠️ Player heartbeat stale (${diff}s old)"
return 1 # Unhealthy
fi
else
# If heartbeat file doesn't exist yet, assume player is starting
return 0
fi
}
# Cleanup function
cleanup() {
log_message "🛑 Watchdog received stop signal"
rm -f "$HEARTBEAT_FILE"
rm -f "$STOP_FLAG_FILE"
exit 0
}
# Trap signals for graceful shutdown
trap cleanup SIGINT SIGTERM
log_message "=========================================="
log_message "🚀 Kivy Signage Player Watchdog Started"
log_message "=========================================="
log_message "Project directory: $SCRIPT_DIR"
log_message "Max retries: Unlimited"
log_message "Restart delay: ${RESTART_DELAY}s"
log_message ""
# Remove old stop flag if exists (fresh start)
rm -f "$STOP_FLAG_FILE"
# Change to the project directory
cd "$SCRIPT_DIR"
echo "Project directory: $SCRIPT_DIR"
# Check if virtual environment exists
if [ -d ".venv" ]; then
echo "Activating virtual environment..."
log_message "✓ Virtual environment found"
source .venv/bin/activate
echo "✓ Virtual environment activated"
else
echo "Warning: Virtual environment not found at .venv/"
echo "Creating virtual environment..."
log_message "⚠️ Creating virtual environment..."
python3 -m venv .venv
source .venv/bin/activate
echo "Installing dependencies..."
log_message "📦 Installing dependencies..."
pip3 install -r requirements.txt
echo "✓ Virtual environment created and dependencies installed"
log_message "✓ Virtual environment ready"
fi
echo ""
# Check if configuration exists
if [ ! -f "config/app_config.txt" ]; then
echo "=========================================="
echo "⚠ WARNING: Configuration file not found!"
echo "=========================================="
echo ""
echo "Please configure the player before running:"
echo " 1. Copy config/app_config.txt.example to config/app_config.txt"
echo " 2. Edit the configuration file with your server details"
echo ""
read -p "Press Enter to continue anyway, or Ctrl+C to exit..."
echo ""
if [ ! -f "config/app_config.json" ]; then
log_message "⚠️ WARNING: Configuration file not found!"
log_message "Player may not function correctly without configuration"
fi
# Change to src directory and start the application
echo "Starting application..."
echo "=========================================="
echo ""
# Main watchdog loop
retry_count=0
while true; do
retry_count=$((retry_count + 1))
log_message ""
log_message "=========================================="
log_message "▶️ Starting player (attempt #${retry_count})"
log_message "=========================================="
# Clean old heartbeat
rm -f "$HEARTBEAT_FILE"
# Start the player
cd "$SCRIPT_DIR/src"
python3 main.py &
PLAYER_PID=$!
log_message "Player PID: $PLAYER_PID"
# Monitor the player
while true; do
sleep $HEALTH_CHECK_INTERVAL
# Check if process is still running
if ! kill -0 $PLAYER_PID 2>/dev/null; then
log_message "❌ Player process crashed or stopped (PID: $PLAYER_PID)"
break
fi
# Check health via heartbeat
if ! check_health; then
log_message "❌ Player health check failed - may be frozen"
kill $PLAYER_PID 2>/dev/null
sleep 2
kill -9 $PLAYER_PID 2>/dev/null
break
fi
# Player is healthy, continue monitoring
done
# Player stopped or crashed
# Check if user requested intentional exit
if [ -f "$STOP_FLAG_FILE" ]; then
log_message "✋ Stop flag detected - user requested exit via password"
log_message "Watchdog will NOT restart the player"
log_message "To restart, run ./start.sh again"
rm -f "$HEARTBEAT_FILE"
break
fi
log_message "⏳ Waiting ${RESTART_DELAY}s before restart..."
sleep $RESTART_DELAY
# Cleanup any zombie processes
pkill -9 -f "python3 main.py" 2>/dev/null
done
cd src
python3 main.py
log_message ""
log_message "=========================================="
log_message "Watchdog stopped"
log_message "=========================================="
# Deactivate virtual environment when app exits
# Deactivate virtual environment (this line is never reached in watchdog mode)
deactivate

34
stop_player.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
# Stop the player and watchdog
# Use this to gracefully shutdown the player
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "=========================================="
echo "Stopping Kivy Signage Player"
echo "=========================================="
echo ""
# Kill watchdog (start.sh)
echo "Stopping watchdog..."
pkill -f "bash.*start.sh"
# Kill player
echo "Stopping player..."
pkill -f "python3 main.py"
# Give processes time to exit gracefully
sleep 2
# Force kill if still running
pkill -9 -f "bash.*start.sh" 2>/dev/null
pkill -9 -f "python3 main.py" 2>/dev/null
# Clean up heartbeat and stop flag files
rm -f "$SCRIPT_DIR/.player_heartbeat"
rm -f "$SCRIPT_DIR/.player_stop_requested"
echo ""
echo "✓ Player and watchdog stopped"
echo ""

View File

@@ -0,0 +1,182 @@
# 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)

View File

@@ -0,0 +1,170 @@
# 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

View File

@@ -0,0 +1,211 @@
# 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!

View File

@@ -0,0 +1,182 @@
# 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!

View File

@@ -0,0 +1,35 @@
#!/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)

View File

@@ -0,0 +1,123 @@
#!/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()

View File

@@ -0,0 +1,167 @@
#!/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()

View File

@@ -0,0 +1,54 @@
#!/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)

View File

@@ -31,15 +31,21 @@ def send_player_feedback(config, message, status="active", playlist_version=None
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):
feedback_url = f'http://{server}:{port}/api/player-feedback'
if re.match(ip_pattern, server_clean):
feedback_url = f'http://{server_clean}:{port}/api/player-feedback'
else:
feedback_url = f'http://{server}/api/player-feedback'
# 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 = {
'player_name': host,
'hostname': host,
'quickconnect_code': quick,
'message': message,
'status': status,
@@ -165,11 +171,17 @@ def fetch_server_playlist(config):
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):
server_url = f'http://{server}:{port}/api/playlists'
if re.match(ip_pattern, server_clean):
server_url = f'http://{server_clean}:{port}/api/playlists'
else:
server_url = f'http://{server}/api/playlists'
# 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

View File

@@ -0,0 +1,10 @@
{
"hostname": "tv-terasa",
"auth_code": "iiSyZDLWGyqNIxeRt54XYREgvAio11RwwU1_oJev6WI",
"player_id": 1,
"player_name": "TV-acasa 1",
"playlist_id": 1,
"orientation": "Landscape",
"authenticated": true,
"server_url": "http://digi-signage.moto-adv.com"
}

View File

@@ -0,0 +1,54 @@
{
"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
}

View File

@@ -0,0 +1,162 @@
#!/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()

View File

@@ -0,0 +1,138 @@
#!/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)

View File

@@ -0,0 +1,55 @@
#!/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)

View File

@@ -0,0 +1,82 @@
#!/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()

View File

@@ -0,0 +1,45 @@
#!/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}")