Fix image editing bug: ensure edits persist and upload correctly

Critical fixes for image editing workflow:

1. Keep local edited files as backup (don't delete after server upload)
   - Server may not process upload immediately
   - Keeps edits safe locally in case server fails
   - Prevents loss of edited images

2. Include original filename in metadata sent to server
   - Server needs to know which file was edited
   - Allows proper tracking and versioning

3. Improved error logging for server upload
   - Now logs detailed errors (404, 401, timeout, connection)
   - Shows clear messages when server doesn't support endpoint
   - Helps diagnose why edits aren't syncing to server

4. Better user feedback during save
   - Shows 'Saved to device' status first
   - Then 'Upload in progress' to show server sync happening
   - Clarifies local vs server save status

Bug symptoms fixed:
- Edited images now persist locally after restart
- Server upload now sends correct file information
- Clear error messages if server upload fails
- User understands 'local save' vs 'server sync' steps
This commit is contained in:
Kiwy Player
2026-01-17 21:44:39 +02:00
parent 120c889143
commit eeb2a61ef7
5 changed files with 80 additions and 108 deletions

1
.player_heartbeat Normal file
View File

@@ -0,0 +1 @@
1768679079.4526675

68
New.txt
View File

@@ -1,68 +0,0 @@
[INFO ] ✓ Playlist is up to date
[WARNING] Deprecated property "<BooleanProperty name=allow_stretch>" of object "<kivy.uix.image.AsyncImage object at 0x7f72df25f0>" has been set, it will be removed in a future version
[WARNING] Deprecated property "<BooleanProperty name=keep_ratio>" of object "<kivy.uix.image.AsyncImage object at 0x7f72df25f0>" was accessed, it will be removed in a future version
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/auth/verify HTTP/1.1" 200 None
[INFO ] ✅ Auth code verified
[INFO ] ✅ Using existing authentication
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/player-feedback HTTP/1.1" 200 None
start.sh: line 212: 16035 Killed python3 main.py
[2026-01-17 21:31:57] ❌ Player process crashed or stopped (PID: 16035)
[2026-01-17 21:31:57] ⏳ Waiting 5s before restart...
[2026-01-17 21:32:02]
[2026-01-17 21:32:02] ==========================================
[2026-01-17 21:32:02] ▶️ Starting player (attempt #2)
[2026-01-17 21:32:02] ==========================================
[2026-01-17 21:32:02] Player PID: 16376
[INFO ] [Logger ] Record log in /home/pi/.kivy/logs/kivy_26-01-17_43.txt
[INFO ] [Kivy ] v2.3.1
[INFO ] [Kivy ] Installed at "/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/kivy/__init__.py"
[INFO ] [Python ] v3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0]
[INFO ] [Python ] Interpreter at "/home/pi/Desktop/Kiwy-Signage/.venv/bin/python3"
[INFO ] [Logger ] Purge log fired. Processing...
[INFO ] [Logger ] Purge finished!
/home/pi/Desktop/Kiwy-Signage/src/main.py:1868: DeprecationWarning: There is no current event loop
loop = asyncio.get_event_loop()
[DEBUG ] [Using selector] EpollSelector
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
WARNING: running xinput against an Xwayland server. See the xinput man page for details.
[ERROR ] [Image ] Error loading </home/pi/Desktop/Kiwy-Signage/config/resources/intro1.mp4>
[WARNING] ⚠️ SSL verification disabled - NOT recommended for production!
[DEBUG ] [Starting new HTTPS connection (1)] 192.168.0.121:443
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/auth/verify HTTP/1.1" 200 None
[INFO ] ✅ Auth code verified
[INFO ] ✅ Using existing authentication
[INFO ] [Fetching playlist from] https://192.168.0.121:443/api/playlists/1
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "GET /api/playlists/1 HTTP/1.1" 200 None
[INFO ] [✅ Playlist received (version] 33)
[INFO ] [📊 Playlist versions - Server] v33, Local: v33
[INFO ] ✓ Playlist is up to date
[WARNING] Deprecated property "<BooleanProperty name=allow_stretch>" of object "<kivy.uix.image.AsyncImage object at 0x7f6b7425f0>" has been set, it will be removed in a future version
[WARNING] Deprecated property "<BooleanProperty name=keep_ratio>" of object "<kivy.uix.image.AsyncImage object at 0x7f6b7425f0>" was accessed, it will be removed in a future version
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/auth/verify HTTP/1.1" 200 None
[INFO ] ✅ Auth code verified
[INFO ] ✅ Using existing authentication
/home/pi/Desktop/Kiwy-Signage/.venv/lib/python3.13/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '192.168.0.121'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
[DEBUG ] [https ]//192.168.0.121:443 "POST /api/player-feedback HTTP/1.1" 200 None
start.sh: line 212: 16376 Killed python3 main.py
[2026-01-17 21:32:32] ❌ Player process crashed or stopped (PID: 16376)
[2026-01-17 21:32:32] ⏳ Waiting 5s before restart...
^C[2026-01-17 21:32:33] 🛑 Watchdog received stop signal
pi@rpi-tvcanba1:~/Desktop/Kiwy-Signage $

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -0,0 +1,8 @@
{
"time_of_modification": "2026-01-17T21:40:13.194566",
"original_name": "2026efvev-1428673176.jpg",
"new_name": "2026efvev-1428673176_e_v1.jpg",
"original_path": "/home/pi/Desktop/Kiwy-Signage/media/2026efvev-1428673176.jpg",
"version": 1,
"user_card_data": "0007206239"
}

View File

@@ -341,10 +341,14 @@ class EditPopup(Popup):
# NOW show saving popup AFTER everything is done
def show_saving_and_dismiss(dt):
# Create label with background
# Create label with background showing detailed save status
save_msg = (
'Saved locally!\n'
'Uploading to server...'
)
save_label = Label(
text='Saved! Reloading player...',
font_size='36sp',
text=save_msg,
font_size='24sp',
color=(1, 1, 1, 1),
bold=True
)
@@ -352,7 +356,7 @@ class EditPopup(Popup):
saving_popup = Popup(
title='',
content=save_label,
size_hint=(0.8, 0.3),
size_hint=(0.85, 0.4),
auto_dismiss=False,
separator_height=0,
background_color=(0.2, 0.7, 0.2, 0.95) # Green background
@@ -360,13 +364,23 @@ class EditPopup(Popup):
saving_popup.open()
Logger.info("EditPopup: Saving confirmation popup opened")
# Dismiss both popups after 2 seconds
# Update message after 3 seconds to show upload is happening
def update_message(dt):
if saving_popup:
save_label.text = (
'✓ Saved to device\n'
'Upload in progress...'
)
Clock.schedule_once(update_message, 3.0)
# Dismiss both popups after 4 seconds
def dismiss_all(dt):
saving_popup.dismiss()
Logger.info(f"EditPopup: Dismissing to resume playback...")
self.dismiss()
Clock.schedule_once(dismiss_all, 2.0)
Clock.schedule_once(dismiss_all, 4.0)
# Small delay to ensure UI is ready, then show popup
Clock.schedule_once(show_saving_and_dismiss, 0.1)
@@ -420,7 +434,8 @@ class EditPopup(Popup):
# 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)")
Logger.warning("EditPopup: Cannot upload - not authenticated (edited media saved locally only)")
Logger.warning("EditPopup: Server will NOT receive this edit")
return False
server_url = auth.auth_data.get('server_url')
@@ -434,60 +449,76 @@ class EditPopup(Popup):
with open(metadata_path, 'r') as meta_file:
metadata = json.load(meta_file)
# Prepare upload URL
# Prepare upload URL - send to the original file endpoint
upload_url = f"{server_url}/api/player-edit-media"
headers = {'Authorization': f'Bearer {auth_code}'}
# Add the original filename to metadata so server knows which file was edited
metadata['original_filename'] = os.path.basename(metadata['original_path'])
# Prepare file and data for upload
with open(image_path, 'rb') as img_file:
files = {
'image_file': (metadata['new_name'], img_file, 'image/jpeg')
'image_file': (metadata['original_filename'], img_file, 'image/jpeg')
}
# Send metadata as JSON string in form data
data = {
'metadata': json.dumps(metadata)
'metadata': json.dumps(metadata),
'original_file': metadata['original_filename']
}
Logger.info(f"EditPopup: Uploading edited media to {upload_url}")
Logger.info(f"EditPopup: 📤 Uploading edited media to {upload_url}")
Logger.info(f"EditPopup: - Original file: {metadata['original_filename']}")
Logger.info(f"EditPopup: - Edited image: {image_path}")
Logger.info(f"EditPopup: - Metadata: {metadata_path}")
try:
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}")
Logger.info(f"EditPopup: ✅ Successfully uploaded edited media to server")
Logger.info(f"EditPopup: Server response: {response_data}")
# Delete local files after successful upload
try:
if os.path.exists(image_path):
os.remove(image_path)
Logger.info(f"EditPopup: Deleted local image file: {os.path.basename(image_path)}")
if os.path.exists(metadata_path):
os.remove(metadata_path)
Logger.info(f"EditPopup: Deleted local metadata file: {os.path.basename(metadata_path)}")
Logger.info("EditPopup: ✅ Local edited files cleaned up after successful upload")
except Exception as e:
Logger.warning(f"EditPopup: Could not delete local files: {e}")
# DO NOT delete local files - keep them as backup
# In case the server doesn't process them, we want to keep the edits locally
Logger.info(f"EditPopup: ✓ Keeping local edited files as backup:")
Logger.info(f" - Image: {image_path}")
Logger.info(f" - Metadata: {metadata_path}")
return True
elif response.status_code == 404:
Logger.warning("EditPopup: ⚠️ Upload endpoint not available on server (404) - edited media saved locally only")
Logger.error("EditPopup: Upload endpoint not found on server (404)")
Logger.error("EditPopup: Server may not support edited media uploads")
Logger.error("EditPopup: Edited media is saved locally only")
return False
elif response.status_code == 401:
Logger.error("EditPopup: ❌ Authentication failed (401) - check auth credentials")
Logger.error("EditPopup: Edited media is saved locally only")
return False
else:
Logger.warning(f"EditPopup: ⚠️ Upload failed with status {response.status_code} - edited media saved locally only")
Logger.error(f"EditPopup: Upload failed with status {response.status_code}")
Logger.error(f"EditPopup: Response: {response.text}")
Logger.error("EditPopup: Edited media is saved locally only")
return False
except requests.exceptions.Timeout:
Logger.warning("EditPopup: ⚠️ Upload timed out - edited media saved locally only")
Logger.error("EditPopup: Upload timed out after 30 seconds")
Logger.error("EditPopup: Check network connection")
Logger.error("EditPopup: Edited media is saved locally only")
return False
except requests.exceptions.ConnectionError:
Logger.warning("EditPopup: ⚠️ Cannot connect to server - edited media saved locally only")
except requests.exceptions.ConnectionError as e:
Logger.error(f"EditPopup: Cannot connect to server: {e}")
Logger.error("EditPopup: Check server URL and network connection")
Logger.error("EditPopup: Edited media is saved locally only")
return False
except Exception as e:
Logger.warning(f"EditPopup: ⚠️ Upload failed: {e} - edited media saved locally only")
Logger.error(f"EditPopup: ❌ Unexpected error during upload: {e}")
import traceback
Logger.debug(f"EditPopup: Upload traceback: {traceback.format_exc()}")
Logger.error(f"EditPopup: Traceback: {traceback.format_exc()}")
Logger.error("EditPopup: Edited media is saved locally only")
return False
def close_without_saving(self, instance):