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)
This commit is contained in:
Kiwy Signage Player
2025-12-06 00:07:48 +02:00
parent f573af0505
commit 89e5ad86dd
4 changed files with 113 additions and 358 deletions

View File

@@ -107,10 +107,11 @@ class DrawingLayer(Widget):
class EditPopup(Popup):
"""Popup for editing/annotating images"""
def __init__(self, player_instance, image_path, **kwargs):
def __init__(self, player_instance, image_path, authenticated_user=None, **kwargs):
super(EditPopup, self).__init__(**kwargs)
self.player = player_instance
self.image_path = image_path
self.authenticated_user = authenticated_user or "player_1" # Default to player_1
self.drawing_layer = None
# Pause playback
@@ -439,11 +440,20 @@ class EditPopup(Popup):
self.right_sidebar.opacity = 1
# Create and save metadata
self._save_metadata(edited_dir, new_name, base_name,
json_filename = self._save_metadata(edited_dir, new_name, base_name,
new_version if version_match else 1, output_filename)
Logger.info(f"EditPopup: Saved edited image to {output_path}")
# Upload to server asynchronously (non-blocking)
import threading
upload_thread = threading.Thread(
target=self._upload_to_server,
args=(output_path, json_filename),
daemon=True
)
upload_thread.start()
# Show confirmation
self.title = f'Saved as {output_filename}'
Clock.schedule_once(lambda dt: self.dismiss(), 1)
@@ -476,7 +486,8 @@ class EditPopup(Popup):
'original_name': base_name,
'new_name': output_filename,
'original_path': self.image_path,
'version': version
'version': version,
'user': self.authenticated_user
}
# Save metadata JSON
@@ -486,6 +497,70 @@ class EditPopup(Popup):
json.dump(metadata, f, indent=2)
Logger.info(f"EditPopup: Saved metadata to {json_path}")
return json_path
def _upload_to_server(self, image_path, metadata_path):
"""Upload edited image and metadata to server (runs in background thread)"""
try:
import requests
import json
from get_playlists_v2 import get_auth_instance
# Get authenticated instance
auth = get_auth_instance()
if not auth or not auth.is_authenticated():
Logger.warning("EditPopup: Cannot upload - not authenticated (server will not receive edited media)")
return False
server_url = auth.auth_data.get('server_url')
auth_code = auth.auth_data.get('auth_code')
if not server_url or not auth_code:
Logger.warning("EditPopup: Missing server URL or auth code (upload skipped)")
return False
# Load metadata from file
with open(metadata_path, 'r') as meta_file:
metadata = json.load(meta_file)
# Prepare upload URL
upload_url = f"{server_url}/api/player-edit-media"
headers = {'Authorization': f'Bearer {auth_code}'}
# Prepare file and data for upload
with open(image_path, 'rb') as img_file:
files = {
'image_file': (metadata['new_name'], img_file, 'image/jpeg')
}
# Send metadata as JSON string in form data
data = {
'metadata': json.dumps(metadata)
}
Logger.info(f"EditPopup: Uploading edited media to {upload_url}")
response = requests.post(upload_url, headers=headers, files=files, data=data, timeout=30)
if response.status_code == 200:
response_data = response.json()
Logger.info(f"EditPopup: ✅ Successfully uploaded edited media to server: {response_data}")
return True
elif response.status_code == 404:
Logger.warning("EditPopup: ⚠️ Upload endpoint not available on server (404) - edited media saved locally only")
return False
else:
Logger.warning(f"EditPopup: ⚠️ Upload failed with status {response.status_code} - edited media saved locally only")
return False
except requests.exceptions.Timeout:
Logger.warning("EditPopup: ⚠️ Upload timed out - edited media saved locally only")
return False
except requests.exceptions.ConnectionError:
Logger.warning("EditPopup: ⚠️ Cannot connect to server - edited media saved locally only")
return False
except Exception as e:
Logger.warning(f"EditPopup: ⚠️ Upload failed: {e} - edited media saved locally only")
return False
def close_without_saving(self, instance):
"""Close without saving"""
@@ -1111,7 +1186,8 @@ class SignagePlayer(Widget):
if self.playlist:
self.ids.status_label.text = f"Playlist loaded: {len(self.playlist)} items"
if not self.is_playing:
# Only start playback if intro has finished
if not self.is_playing and self.intro_played:
Clock.schedule_once(self.start_playback, 1)
else:
self.ids.status_label.text = "No media in playlist"
@@ -1253,13 +1329,13 @@ class SignagePlayer(Widget):
# Video file
Logger.info(f"SignagePlayer: Media type: VIDEO")
self.play_video(media_path, duration)
elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif']:
elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']:
# Image file
Logger.info(f"SignagePlayer: Media type: IMAGE")
self.play_image(media_path, duration)
else:
Logger.warning(f"SignagePlayer: ❌ Unsupported media type: {file_extension}")
Logger.warning(f"SignagePlayer: Supported: .mp4/.avi/.mkv/.mov/.webm/.jpg/.jpeg/.png/.bmp/.gif")
Logger.warning(f"SignagePlayer: Supported: .mp4/.avi/.mkv/.mov/.webm/.jpg/.jpeg/.png/.bmp/.gif/.webp")
Logger.warning(f"SignagePlayer: Skipping to next media...")
self.consecutive_errors += 1
self.next_media()
@@ -1527,8 +1603,8 @@ class SignagePlayer(Widget):
file_name = media_item.get('file_name', '')
file_extension = os.path.splitext(file_name)[1].lower()
# Only allow editing images
if file_extension not in ['.jpg', '.jpeg', '.png', '.bmp']:
# Check 1: Only allow editing images
if file_extension not in ['.jpg', '.jpeg', '.png', '.bmp', '.webp']:
Logger.warning(f"SignagePlayer: Cannot edit {file_extension} files, only images")
# Show error message briefly
self.ids.status_label.text = 'Can only edit image files'
@@ -1536,6 +1612,28 @@ class SignagePlayer(Widget):
Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2)
return
# Check 2: Verify edit_on_player permission from server
edit_allowed = media_item.get('edit_on_player', False)
if not edit_allowed:
Logger.warning(f"SignagePlayer: Edit not allowed for {file_name} (edit_on_player=false)")
# Show error message briefly
self.ids.status_label.text = 'Edit not permitted for this media'
self.ids.status_label.opacity = 1
Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2)
return
# Check 3: Verify user authentication
# TODO: Implement card swipe authentication system
authenticated_user = "player_1" # Placeholder - will be replaced with card authentication
if not authenticated_user:
Logger.warning(f"SignagePlayer: User not authenticated for editing")
# Show error message briefly
self.ids.status_label.text = 'User authentication required'
self.ids.status_label.opacity = 1
Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2)
return
# Get full path to current image
image_path = os.path.join(self.media_dir, file_name)
@@ -1543,10 +1641,10 @@ class SignagePlayer(Widget):
Logger.error(f"SignagePlayer: Image not found: {image_path}")
return
Logger.info(f"SignagePlayer: Opening edit interface for {file_name}")
Logger.info(f"SignagePlayer: Opening edit interface for {file_name} (user: {authenticated_user})")
# Open edit popup
popup = EditPopup(player_instance=self, image_path=image_path)
# Open edit popup with authenticated user
popup = EditPopup(player_instance=self, image_path=image_path, authenticated_user=authenticated_user)
popup.open()
def show_exit_popup(self, instance=None):