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:
120
src/main.py
120
src/main.py
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user