Cleanup repo + harden offline install and watchdog
Repo hygiene: - Remove dead scripts (.wait-for-display, .display-keepalive, .keep-screen-alive) - Remove stale backup/scratch (install.sh.bak, New.txt, stale .pyc) - Stop tracking machine-specific/generated state (config/app_config.json, playlists/server_playlist.json, src/player_auth.json, generated launchers) - Prune duplicate wheel versions (one per package) - Reorganize working_files -> dev/, dev/tests/, documentation/dev-notes/, scripts/ - Extend .gitignore; fix no-op rules in repo/.gitignore Offline install (install.sh): - Non-fatal system-dep step; per-package apt retry; .deb bundle install - Skip pip self-upgrade offline; guard online fallbacks behind connectivity probe - Dependency-free fetch_url (curl/wget/urllib); --no-reboot flag - Fix corrupted literal backslash-bang escapes (shebangs, test -f, banner) - Idempotent cron install (no more stacked @reboot lines) Autostart / watchdog: - start.sh: single-instance flock lock (.player_watchdog.lock) acquired only after a display is confirmed, preventing parallel watchdogs - Set XDG_RUNTIME_DIR when missing; exit early if headless - run_player.sh: activate .venv; stop_player.sh: clear lock file
This commit is contained in:
@@ -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)
|
||||
@@ -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()
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
# Video Conversion Script for Raspberry Pi Signage Player
|
||||
# Converts videos to optimal settings: 1080p @ 30fps, H.264 codec
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <input_video> [output_video]"
|
||||
echo "Example: $0 input.mp4 output.mp4"
|
||||
echo ""
|
||||
echo "This script converts videos to Raspberry Pi-friendly settings:"
|
||||
echo " - Resolution: Max 1920x1080"
|
||||
echo " - Frame rate: 30 fps"
|
||||
echo " - Codec: H.264"
|
||||
echo " - Bitrate: ~5-8 Mbps"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT_VIDEO="$1"
|
||||
OUTPUT_VIDEO="${2:-converted_$(basename "$INPUT_VIDEO")}"
|
||||
|
||||
if [ ! -f "$INPUT_VIDEO" ]; then
|
||||
echo "Error: Input file '$INPUT_VIDEO' not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Converting video for Raspberry Pi playback..."
|
||||
echo "Input: $INPUT_VIDEO"
|
||||
echo "Output: $OUTPUT_VIDEO"
|
||||
echo ""
|
||||
|
||||
# Convert video with optimal settings for Raspberry Pi
|
||||
ffmpeg -i "$INPUT_VIDEO" \
|
||||
-c:v libx264 \
|
||||
-preset medium \
|
||||
-crf 23 \
|
||||
-maxrate 8M \
|
||||
-bufsize 12M \
|
||||
-vf "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease,fps=30" \
|
||||
-r 30 \
|
||||
-c:a aac \
|
||||
-b:a 128k \
|
||||
-movflags +faststart \
|
||||
-y \
|
||||
"$OUTPUT_VIDEO"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
echo "✓ Conversion completed successfully!"
|
||||
echo "Original: $(du -h "$INPUT_VIDEO" | cut -f1)"
|
||||
echo "Converted: $(du -h "$OUTPUT_VIDEO" | cut -f1)"
|
||||
echo ""
|
||||
echo "You can now use '$OUTPUT_VIDEO' in your signage player."
|
||||
else
|
||||
echo ""
|
||||
echo "✗ Conversion failed! Make sure ffmpeg is installed:"
|
||||
echo " sudo apt-get install ffmpeg"
|
||||
exit 1
|
||||
fi
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -0,0 +1,346 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import bcrypt
|
||||
import re
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def send_player_feedback(config, message, status="active", playlist_version=None, error_details=None):
|
||||
"""
|
||||
Send feedback to the server about player status.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
message (str): Main feedback message
|
||||
status (str): Player status - "active", "playing", "error", "restarting"
|
||||
playlist_version (int, optional): Current playlist version being played
|
||||
error_details (str, optional): Error details if status is "error"
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
server = config.get("server_ip", "")
|
||||
host = config.get("screen_name", "")
|
||||
quick = config.get("quickconnect_key", "")
|
||||
port = config.get("port", "")
|
||||
|
||||
# Construct server URL
|
||||
# Remove protocol if already present
|
||||
server_clean = server.replace('http://', '').replace('https://', '')
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server_clean):
|
||||
feedback_url = f'http://{server_clean}:{port}/api/player-feedback'
|
||||
else:
|
||||
# Use original server if it has protocol, otherwise add http://
|
||||
if server.startswith(('http://', 'https://')):
|
||||
feedback_url = f'{server}/api/player-feedback'
|
||||
else:
|
||||
feedback_url = f'http://{server}/api/player-feedback'
|
||||
|
||||
# Prepare feedback data
|
||||
feedback_data = {
|
||||
'hostname': host,
|
||||
'quickconnect_code': quick,
|
||||
'message': message,
|
||||
'status': status,
|
||||
'timestamp': datetime.datetime.now().isoformat(),
|
||||
'playlist_version': playlist_version,
|
||||
'error_details': error_details
|
||||
}
|
||||
|
||||
logger.info(f"Sending feedback to {feedback_url}: {feedback_data}")
|
||||
|
||||
# Send POST request
|
||||
response = requests.post(feedback_url, json=feedback_data, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
logger.info(f"Feedback sent successfully: {message}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Feedback failed with status {response.status_code}: {response.text}")
|
||||
return False
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to send feedback: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error sending feedback: {e}")
|
||||
return False
|
||||
|
||||
def send_playlist_check_feedback(config, playlist_version=None):
|
||||
"""
|
||||
Send feedback when playlist is checked for updates.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, is active, Playing {version_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="active",
|
||||
playlist_version=playlist_version
|
||||
)
|
||||
|
||||
def send_playlist_restart_feedback(config, playlist_version=None):
|
||||
"""
|
||||
Send feedback when playlist loop ends and restarts.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"v{playlist_version}" if playlist_version else "unknown"
|
||||
message = f"player {player_name}, playlist loop completed, restarting {version_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="restarting",
|
||||
playlist_version=playlist_version
|
||||
)
|
||||
|
||||
def send_player_error_feedback(config, error_message, playlist_version=None):
|
||||
"""
|
||||
Send feedback when an error occurs in the player.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
error_message (str): Description of the error
|
||||
playlist_version (int, optional): Current playlist version
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
message = f"player {player_name}, error occurred"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="error",
|
||||
playlist_version=playlist_version,
|
||||
error_details=error_message
|
||||
)
|
||||
|
||||
def send_playing_status_feedback(config, playlist_version=None, current_media=None):
|
||||
"""
|
||||
Send feedback about current playing status.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration containing server details
|
||||
playlist_version (int, optional): Current playlist version
|
||||
current_media (str, optional): Currently playing media file
|
||||
|
||||
Returns:
|
||||
bool: True if feedback sent successfully, False otherwise
|
||||
"""
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
version_info = f"v{playlist_version}" if playlist_version else "unknown"
|
||||
media_info = f" - {current_media}" if current_media else ""
|
||||
message = f"player {player_name}, is active, Playing {version_info}{media_info}"
|
||||
|
||||
return send_player_feedback(
|
||||
config=config,
|
||||
message=message,
|
||||
status="playing",
|
||||
playlist_version=playlist_version
|
||||
)
|
||||
|
||||
def fetch_server_playlist(config):
|
||||
"""Fetch the updated playlist from the server using a config dict."""
|
||||
server = config.get("server_ip", "")
|
||||
host = config.get("screen_name", "")
|
||||
quick = config.get("quickconnect_key", "")
|
||||
port = config.get("port", "")
|
||||
try:
|
||||
# Remove protocol if already present
|
||||
server_clean = server.replace('http://', '').replace('https://', '')
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server_clean):
|
||||
server_url = f'http://{server_clean}:{port}/api/playlists'
|
||||
else:
|
||||
# Use original server if it has protocol, otherwise add http://
|
||||
if server.startswith(('http://', 'https://')):
|
||||
server_url = f'{server}/api/playlists'
|
||||
else:
|
||||
server_url = f'http://{server}/api/playlists'
|
||||
params = {
|
||||
'hostname': host,
|
||||
'quickconnect_code': quick
|
||||
}
|
||||
logger.info(f"Fetching playlist from URL: {server_url} with params: {params}")
|
||||
response = requests.get(server_url, params=params)
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
logger.info(f"Server response: {response_data}")
|
||||
playlist = response_data.get('playlist', [])
|
||||
version = response_data.get('playlist_version', None)
|
||||
hashed_quickconnect = response_data.get('hashed_quickconnect', None)
|
||||
if version is not None and hashed_quickconnect is not None:
|
||||
if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')):
|
||||
logger.info("Fetched updated playlist from server.")
|
||||
return {'playlist': playlist, 'version': version}
|
||||
else:
|
||||
logger.error("Quickconnect code validation failed.")
|
||||
else:
|
||||
logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
|
||||
else:
|
||||
logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to fetch playlist: {e}")
|
||||
return {'playlist': [], 'version': 0}
|
||||
|
||||
def save_playlist_with_version(playlist_data, playlist_dir):
|
||||
version = playlist_data.get('version', 0)
|
||||
playlist_file = os.path.join(playlist_dir, f'server_playlist_v{version}.json')
|
||||
with open(playlist_file, 'w') as f:
|
||||
json.dump(playlist_data, f, indent=2)
|
||||
logger.info(f"Playlist saved to {playlist_file}")
|
||||
return playlist_file
|
||||
|
||||
def download_media_files(playlist, media_dir):
|
||||
"""Download media files from the server and save them to media_dir."""
|
||||
if not os.path.exists(media_dir):
|
||||
os.makedirs(media_dir)
|
||||
logger.info(f"Created directory {media_dir} for media files.")
|
||||
|
||||
updated_playlist = []
|
||||
for media in playlist:
|
||||
file_name = media.get('file_name', '')
|
||||
file_url = media.get('url', '')
|
||||
duration = media.get('duration', 10)
|
||||
local_path = os.path.join(media_dir, file_name)
|
||||
logger.info(f"Preparing to download {file_name} from {file_url}...")
|
||||
if os.path.exists(local_path):
|
||||
logger.info(f"File {file_name} already exists. Skipping download.")
|
||||
else:
|
||||
try:
|
||||
response = requests.get(file_url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
with open(local_path, 'wb') as file:
|
||||
file.write(response.content)
|
||||
logger.info(f"Successfully downloaded {file_name} to {local_path}")
|
||||
else:
|
||||
logger.error(f"Failed to download {file_name}. Status Code: {response.status_code}")
|
||||
continue
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Error downloading {file_name}: {e}")
|
||||
continue
|
||||
updated_media = {
|
||||
'file_name': file_name,
|
||||
'url': os.path.relpath(local_path, os.path.dirname(media_dir)),
|
||||
'duration': duration
|
||||
}
|
||||
updated_playlist.append(updated_media)
|
||||
return updated_playlist
|
||||
|
||||
def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, keep_versions=1):
|
||||
"""
|
||||
Delete old playlist files and media files not referenced by the latest playlist version.
|
||||
keep_versions: number of latest versions to keep (default 1)
|
||||
"""
|
||||
# Find all playlist files
|
||||
playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')]
|
||||
# Keep only the latest N versions
|
||||
versions = sorted([int(f.split('_v')[-1].split('.json')[0]) for f in playlist_files], reverse=True)
|
||||
keep = set(versions[:keep_versions])
|
||||
# Delete old playlist files
|
||||
for f in playlist_files:
|
||||
v = int(f.split('_v')[-1].split('.json')[0])
|
||||
if v not in keep:
|
||||
os.remove(os.path.join(playlist_dir, f))
|
||||
# Collect all media files referenced by the kept playlists
|
||||
referenced = set()
|
||||
for v in keep:
|
||||
path = os.path.join(playlist_dir, f'server_playlist_v{v}.json')
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
data = json.load(f)
|
||||
for item in data.get('playlist', []):
|
||||
referenced.add(item.get('file_name'))
|
||||
# Delete media files not referenced
|
||||
for f in os.listdir(media_dir):
|
||||
if f not in referenced:
|
||||
try:
|
||||
os.remove(os.path.join(media_dir, f))
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete media file {f}: {e}")
|
||||
|
||||
def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir):
|
||||
"""
|
||||
Fetch the server playlist once, compare versions, and update if needed.
|
||||
Returns True if updated, False if already up to date.
|
||||
Also sends feedback to server about playlist check.
|
||||
"""
|
||||
server_data = fetch_server_playlist(config)
|
||||
server_version = server_data.get('version', 0)
|
||||
if not os.path.exists(local_playlist_path):
|
||||
local_version = 0
|
||||
else:
|
||||
with open(local_playlist_path, 'r') as f:
|
||||
local_data = json.load(f)
|
||||
local_version = local_data.get('version', 0)
|
||||
|
||||
logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}")
|
||||
|
||||
# Send feedback about playlist check
|
||||
send_playlist_check_feedback(config, server_version if server_version > 0 else local_version)
|
||||
|
||||
if local_version != server_version:
|
||||
if server_data and server_data.get('playlist'):
|
||||
updated_playlist = download_media_files(server_data['playlist'], media_dir)
|
||||
server_data['playlist'] = updated_playlist
|
||||
save_playlist_with_version(server_data, playlist_dir)
|
||||
# Delete old playlists and unreferenced media
|
||||
delete_old_playlists_and_media(server_version, playlist_dir, media_dir)
|
||||
|
||||
# Send feedback about playlist update
|
||||
player_name = config.get("screen_name", "unknown")
|
||||
update_message = f"player {player_name}, playlist updated to v{server_version}"
|
||||
send_player_feedback(config, update_message, "active", server_version)
|
||||
|
||||
return True
|
||||
else:
|
||||
logger.warning("No playlist data fetched from server or playlist is empty.")
|
||||
|
||||
# Send error feedback
|
||||
send_player_error_feedback(config, "No playlist data fetched from server or playlist is empty", local_version)
|
||||
|
||||
return False
|
||||
else:
|
||||
logger.info("Local playlist is already up to date.")
|
||||
return False
|
||||
|
||||
def is_playlist_up_to_date(local_playlist_path, config):
|
||||
"""
|
||||
Compare the version of the local playlist with the server playlist.
|
||||
Returns True if up-to-date, False otherwise.
|
||||
"""
|
||||
if not os.path.exists(local_playlist_path):
|
||||
logger.info(f"Local playlist file not found: {local_playlist_path}")
|
||||
return False
|
||||
with open(local_playlist_path, 'r') as f:
|
||||
local_data = json.load(f)
|
||||
local_version = local_data.get('version', 0)
|
||||
server_data = fetch_server_playlist(config)
|
||||
server_version = server_data.get('version', 0)
|
||||
logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}")
|
||||
return local_version == server_version
|
||||
@@ -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
|
||||
}
|
||||
Executable
+185
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for Kiwy-Signage authentication with DigiServer v2
|
||||
Run this to verify authentication is working before updating main.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add src directory to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from player_auth import PlayerAuth
|
||||
import json
|
||||
|
||||
def load_app_config():
|
||||
"""Load existing app_config.json"""
|
||||
# Try multiple possible locations
|
||||
possible_paths = [
|
||||
'config/app_config.json',
|
||||
'resources/app_config.txt',
|
||||
'src/config/app_config.json',
|
||||
'../config/app_config.json'
|
||||
]
|
||||
|
||||
for config_file in possible_paths:
|
||||
if os.path.exists(config_file):
|
||||
print(f" Found config: {config_file}")
|
||||
with open(config_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
print(f"❌ Config file not found! Tried:")
|
||||
for path in possible_paths:
|
||||
print(f" - {path}")
|
||||
return None
|
||||
|
||||
def test_authentication():
|
||||
"""Test authentication with DigiServer v2"""
|
||||
print("=" * 60)
|
||||
print("Kiwy-Signage Authentication Test")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Load config
|
||||
print("📁 Loading configuration...")
|
||||
config = load_app_config()
|
||||
if not config:
|
||||
return False
|
||||
|
||||
server_ip = config.get('server_ip', '')
|
||||
hostname = config.get('screen_name', '')
|
||||
quickconnect = config.get('quickconnect_key', '')
|
||||
port = config.get('port', '')
|
||||
|
||||
print(f" Server: {server_ip}:{port}")
|
||||
print(f" Hostname: {hostname}")
|
||||
print(f" Quick Connect: {'*' * len(quickconnect)}")
|
||||
print()
|
||||
|
||||
# Build server URL
|
||||
import re
|
||||
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
|
||||
if re.match(ip_pattern, server_ip):
|
||||
server_url = f'http://{server_ip}:{port}'
|
||||
else:
|
||||
server_url = f'http://{server_ip}'
|
||||
|
||||
print(f"🌐 Server URL: {server_url}")
|
||||
print()
|
||||
|
||||
# Test server connection
|
||||
print("🔌 Testing server connection...")
|
||||
try:
|
||||
import requests
|
||||
response = requests.get(f"{server_url}/api/health", timeout=5)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" ✅ Server is healthy (version: {data.get('version')})")
|
||||
else:
|
||||
print(f" ⚠️ Server responded with status: {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Cannot connect to server: {e}")
|
||||
print()
|
||||
print("💡 Make sure DigiServer v2 is running and accessible!")
|
||||
return False
|
||||
print()
|
||||
|
||||
# Initialize auth
|
||||
print("🔐 Initializing authentication...")
|
||||
auth = PlayerAuth(config_file='src/player_auth.json')
|
||||
|
||||
# Check if already authenticated
|
||||
if auth.is_authenticated():
|
||||
print(f" ℹ️ Found existing authentication")
|
||||
print(f" Player: {auth.get_player_name()}")
|
||||
print()
|
||||
|
||||
print("✓ Verifying saved authentication...")
|
||||
valid, info = auth.verify_auth()
|
||||
|
||||
if valid:
|
||||
print(f" ✅ Authentication is valid!")
|
||||
print(f" Player ID: {info['player_id']}")
|
||||
print(f" Player Name: {info['player_name']}")
|
||||
print(f" Group ID: {info.get('group_id', 'None')}")
|
||||
print(f" Orientation: {info.get('orientation', 'Landscape')}")
|
||||
print()
|
||||
|
||||
# Test playlist fetch
|
||||
print("📋 Testing playlist fetch...")
|
||||
playlist_data = auth.get_playlist()
|
||||
if playlist_data:
|
||||
version = playlist_data.get('playlist_version', 0)
|
||||
content_count = len(playlist_data.get('playlist', []))
|
||||
print(f" ✅ Playlist received!")
|
||||
print(f" Version: {version}")
|
||||
print(f" Content items: {content_count}")
|
||||
else:
|
||||
print(f" ⚠️ Could not fetch playlist")
|
||||
print()
|
||||
|
||||
# Test heartbeat
|
||||
print("💓 Testing heartbeat...")
|
||||
if auth.send_heartbeat(status='online'):
|
||||
print(f" ✅ Heartbeat sent successfully")
|
||||
else:
|
||||
print(f" ⚠️ Heartbeat failed")
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print("✅ All tests passed! Player is ready to use.")
|
||||
print("=" * 60)
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ Saved authentication is expired or invalid")
|
||||
print(f" Re-authenticating...")
|
||||
print()
|
||||
|
||||
# Need to authenticate
|
||||
print("🔑 Authenticating with server...")
|
||||
success, error = auth.authenticate(
|
||||
server_url=server_url,
|
||||
hostname=hostname,
|
||||
quickconnect_code=quickconnect
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f" ✅ Authentication successful!")
|
||||
print(f" Player: {auth.get_player_name()}")
|
||||
print(f" Player ID: {auth.get_player_id()}")
|
||||
print()
|
||||
|
||||
# Save confirmation
|
||||
print(f"💾 Authentication saved to: src/player_auth.json")
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print("✅ Authentication successful! Player is ready to use.")
|
||||
print("=" * 60)
|
||||
return True
|
||||
else:
|
||||
print(f" ❌ Authentication failed: {error}")
|
||||
print()
|
||||
print("💡 Troubleshooting:")
|
||||
print(" 1. Check player exists in DigiServer v2 (hostname must match)")
|
||||
print(" 2. Verify quickconnect_key matches server configuration")
|
||||
print(" 3. Check server logs for authentication attempts")
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("❌ Authentication test failed")
|
||||
print("=" * 60)
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
success = test_authentication()
|
||||
sys.exit(0 if success else 1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ Test cancelled by user")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Unexpected error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to diagnose edited media upload issues
|
||||
Run this to test if the server endpoint exists and works correctly
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# Add src to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
def test_upload():
|
||||
"""Test the edited media upload functionality"""
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("EDITED MEDIA UPLOAD DIAGNOSTICS")
|
||||
print("="*60)
|
||||
|
||||
# Get authentication
|
||||
try:
|
||||
from get_playlists_v2 import get_auth_instance
|
||||
auth = get_auth_instance()
|
||||
|
||||
if not auth or not auth.is_authenticated():
|
||||
print("❌ ERROR: Not authenticated!")
|
||||
print(" Please ensure player_auth.json exists and is valid")
|
||||
return False
|
||||
|
||||
server_url = auth.auth_data.get('server_url')
|
||||
auth_code = auth.auth_data.get('auth_code')
|
||||
|
||||
print(f"\n✓ Authentication successful")
|
||||
print(f" Server URL: {server_url}")
|
||||
print(f" Auth Code: {auth_code[:20]}..." if auth_code else " Auth Code: None")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Authentication error: {e}")
|
||||
return False
|
||||
|
||||
# Check for edited media files
|
||||
edited_media_dir = os.path.join(os.path.dirname(__file__), 'media', 'edited_media')
|
||||
edited_files = list(Path(edited_media_dir).glob('*_e_v*.jpg'))
|
||||
metadata_files = list(Path(edited_media_dir).glob('*_metadata.json'))
|
||||
|
||||
print(f"\n✓ Edited Media Directory: {edited_media_dir}")
|
||||
print(f" Edited images: {len(edited_files)}")
|
||||
print(f" Metadata files: {len(metadata_files)}")
|
||||
|
||||
if not edited_files:
|
||||
print("\n⚠️ No edited images found!")
|
||||
print(" Create an edit first, then run this test")
|
||||
return False
|
||||
|
||||
# Test with the first edited image
|
||||
image_path = str(edited_files[0])
|
||||
metadata_file = str(edited_files[0]).replace('.jpg', '_metadata.json')
|
||||
|
||||
if not os.path.exists(metadata_file):
|
||||
print(f"\n❌ Metadata file not found: {metadata_file}")
|
||||
return False
|
||||
|
||||
print(f"\nTesting upload with:")
|
||||
print(f" Image: {os.path.basename(image_path)}")
|
||||
print(f" Size: {os.path.getsize(image_path):,} bytes")
|
||||
print(f" Metadata: {os.path.basename(metadata_file)}")
|
||||
|
||||
# Load and display metadata
|
||||
with open(metadata_file, 'r') as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
print(f"\nMetadata content:")
|
||||
for key, value in metadata.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
# Add original_filename if not present
|
||||
metadata['original_filename'] = os.path.basename(metadata['original_path'])
|
||||
|
||||
# Prepare upload request
|
||||
upload_url = f"{server_url}/api/player-edit-media"
|
||||
headers = {'Authorization': f'Bearer {auth_code}'}
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("TESTING UPLOAD...")
|
||||
print(f"{'='*60}")
|
||||
print(f"Endpoint: {upload_url}")
|
||||
print(f"Headers: Authorization: Bearer {auth_code[:20]}...")
|
||||
|
||||
try:
|
||||
with open(image_path, 'rb') as img_file:
|
||||
files = {
|
||||
'image_file': (metadata['original_filename'], img_file, 'image/jpeg')
|
||||
}
|
||||
data = {
|
||||
'metadata': json.dumps(metadata),
|
||||
'original_file': metadata['original_filename']
|
||||
}
|
||||
|
||||
print(f"\nSending request (30s timeout, SSL verify=False)...")
|
||||
response = requests.post(
|
||||
upload_url,
|
||||
headers=headers,
|
||||
files=files,
|
||||
data=data,
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
|
||||
print(f"\n✓ Response received!")
|
||||
print(f" Status Code: {response.status_code}")
|
||||
print(f" Headers: {dict(response.headers)}")
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"\n✅ SUCCESS! Server accepted the upload")
|
||||
print(f" Response: {response.json()}")
|
||||
return True
|
||||
elif response.status_code == 404:
|
||||
print(f"\n❌ ENDPOINT NOT FOUND (404)")
|
||||
print(f" The server does NOT have /api/player-edit-media endpoint")
|
||||
print(f" Server may need to implement this feature")
|
||||
elif response.status_code == 401:
|
||||
print(f"\n❌ AUTHENTICATION FAILED (401)")
|
||||
print(f" Check your auth_code in player_auth.json")
|
||||
else:
|
||||
print(f"\n❌ REQUEST FAILED (Status: {response.status_code})")
|
||||
print(f" Response: {response.text}")
|
||||
|
||||
return False
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"\n❌ CONNECTION ERROR")
|
||||
print(f" Cannot reach server at {server_url}")
|
||||
print(f" Error: {e}")
|
||||
return False
|
||||
except requests.exceptions.Timeout as e:
|
||||
print(f"\n❌ TIMEOUT")
|
||||
print(f" Server did not respond within 30 seconds")
|
||||
print(f" Error: {e}")
|
||||
return False
|
||||
except requests.exceptions.SSLError as e:
|
||||
print(f"\n❌ SSL ERROR")
|
||||
print(f" Error: {e}")
|
||||
print(f" Tip: Try adding verify=False to requests")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"\n❌ UNEXPECTED ERROR")
|
||||
print(f" Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = test_upload()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
if success:
|
||||
print("✅ UPLOAD TEST PASSED - Server accepts edited media!")
|
||||
else:
|
||||
print("❌ UPLOAD TEST FAILED - See details above")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
Reference in New Issue
Block a user