From 8e2309ac583c05bb2b479bc97b799c1989826921 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 17 Jul 2025 21:02:50 -0500 Subject: [PATCH] Fixes logo uploads --- apps/channels/api_views.py | 14 +++++++- dispatcharr/utils.py | 8 ++--- frontend/src/api.js | 42 +++++++++++++++++++--- frontend/src/components/forms/Channel.jsx | 27 +++++++++++--- frontend/src/components/forms/Channels.jsx | 27 +++++++++++--- frontend/src/components/forms/Logo.jsx | 23 +++++++++--- 6 files changed, 119 insertions(+), 22 deletions(-) diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index 0956da11..ee7109b7 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -1172,6 +1172,16 @@ class LogoViewSet(viewsets.ModelViewSet): ) file = request.FILES["file"] + + # Validate file + try: + from dispatcharr.utils import validate_logo_file + validate_logo_file(file) + except Exception as e: + return Response( + {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST + ) + file_name = file.name file_path = os.path.join("/data/logos", file_name) @@ -1187,8 +1197,10 @@ class LogoViewSet(viewsets.ModelViewSet): }, ) + # Use get_serializer to ensure proper context + serializer = self.get_serializer(logo) return Response( - LogoSerializer(logo, context={'request': request}).data, + serializer.data, status=status.HTTP_201_CREATED, ) diff --git a/dispatcharr/utils.py b/dispatcharr/utils.py index 767913c6..5e1ad087 100644 --- a/dispatcharr/utils.py +++ b/dispatcharr/utils.py @@ -21,11 +21,11 @@ def json_success_response(data=None, status=200): def validate_logo_file(file): """Validate uploaded logo file size and MIME type.""" - valid_mime_types = ["image/jpeg", "image/png", "image/gif"] + valid_mime_types = ["image/jpeg", "image/png", "image/gif", "image/webp"] if file.content_type not in valid_mime_types: - raise ValidationError("Unsupported file type. Allowed types: JPEG, PNG, GIF.") - if file.size > 2 * 1024 * 1024: - raise ValidationError("File too large. Max 2MB.") + raise ValidationError("Unsupported file type. Allowed types: JPEG, PNG, GIF, WebP.") + if file.size > 5 * 1024 * 1024: # Increased to 5MB + raise ValidationError("File too large. Max 5MB.") def get_client_ip(request): diff --git a/frontend/src/api.js b/frontend/src/api.js index 967e462b..bcffc920 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -209,10 +209,10 @@ export default class API { API.getAllChannelIds(API.lastQueryParams), ]); - useChannelsTable + useChannelsTableStore .getState() .queryChannels(response, API.lastQueryParams); - useChannelsTable.getState().setAllQueryIds(ids); + useChannelsTableStore.getState().setAllQueryIds(ids); return response; } catch (e) { @@ -1252,16 +1252,48 @@ export default class API { const formData = new FormData(); formData.append('file', file); - const response = await request(`${host}/api/channels/logos/upload/`, { + // Add timeout handling for file uploads + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout + + const response = await fetch(`${host}/api/channels/logos/upload/`, { method: 'POST', body: formData, + headers: { + Authorization: `Bearer ${await API.getAuthToken()}`, + }, + signal: controller.signal, }); - useChannelsStore.getState().addLogo(response); + clearTimeout(timeoutId); - return response; + if (!response.ok) { + const error = new Error(`HTTP error! Status: ${response.status}`); + let errorBody = await response.text(); + + try { + errorBody = JSON.parse(errorBody); + } catch (e) { + // If parsing fails, leave errorBody as the raw text + } + + error.status = response.status; + error.response = response; + error.body = errorBody; + throw error; + } + + const result = await response.json(); + useChannelsStore.getState().addLogo(result); + return result; } catch (e) { + if (e.name === 'AbortError') { + const timeoutError = new Error('Upload timed out. Please try again.'); + timeoutError.code = 'NETWORK_ERROR'; + throw timeoutError; + } errorNotification('Failed to upload logo', e); + throw e; } } diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx index 64412cb4..c7d8ed6c 100644 --- a/frontend/src/components/forms/Channel.jsx +++ b/frontend/src/components/forms/Channel.jsx @@ -31,6 +31,7 @@ import { Image, UnstyledButton, } from '@mantine/core'; +import { notifications } from '@mantine/notifications'; import { ListOrdered, SquarePlus, SquareX, X } from 'lucide-react'; import useEPGsStore from '../../store/epgs'; import { Dropzone } from '@mantine/dropzone'; @@ -84,10 +85,28 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => { const handleLogoChange = async (files) => { if (files.length === 1) { - const retval = await API.uploadLogo(files[0]); - await fetchLogos(); - setLogoPreview(retval.cache_url); - formik.setFieldValue('logo_id', retval.id); + const file = files[0]; + + // Validate file size on frontend first + if (file.size > 5 * 1024 * 1024) { + // 5MB + notifications.show({ + title: 'Error', + message: 'File too large. Maximum size is 5MB.', + color: 'red', + }); + return; + } + + try { + const retval = await API.uploadLogo(file); + await fetchLogos(); + setLogoPreview(retval.cache_url); + formik.setFieldValue('logo_id', retval.id); + } catch (error) { + console.error('Logo upload failed:', error); + // Error notification is already handled in API.uploadLogo + } } else { setLogoPreview(null); } diff --git a/frontend/src/components/forms/Channels.jsx b/frontend/src/components/forms/Channels.jsx index dbce5cf3..e67d9419 100644 --- a/frontend/src/components/forms/Channels.jsx +++ b/frontend/src/components/forms/Channels.jsx @@ -34,6 +34,7 @@ import { import { ListOrdered, SquarePlus, SquareX, X } from 'lucide-react'; import useEPGsStore from '../../store/epgs'; import { Dropzone } from '@mantine/dropzone'; +import { notifications } from '@mantine/notifications'; import { FixedSizeList as List } from 'react-window'; const ChannelsForm = ({ channel = null, isOpen, onClose }) => { @@ -81,10 +82,28 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => { const handleLogoChange = async (files) => { if (files.length === 1) { - const retval = await API.uploadLogo(files[0]); - await fetchLogos(); - setLogoPreview(retval.cache_url); - formik.setFieldValue('logo_id', retval.id); + const file = files[0]; + + // Validate file size on frontend first + if (file.size > 5 * 1024 * 1024) { + // 5MB + notifications.show({ + title: 'Error', + message: 'File too large. Maximum size is 5MB.', + color: 'red', + }); + return; + } + + try { + const retval = await API.uploadLogo(file); + await fetchLogos(); + setLogoPreview(retval.cache_url); + formik.setFieldValue('logo_id', retval.id); + } catch (error) { + console.error('Logo upload failed:', error); + // Error notification is already handled in API.uploadLogo + } } else { setLogoPreview(null); } diff --git a/frontend/src/components/forms/Logo.jsx b/frontend/src/components/forms/Logo.jsx index c3e48d5d..436dbf8a 100644 --- a/frontend/src/components/forms/Logo.jsx +++ b/frontend/src/components/forms/Logo.jsx @@ -51,12 +51,12 @@ const LogoForm = ({ logo = null, isOpen, onClose }) => { onClose(); } catch (error) { let errorMessage = logo ? 'Failed to update logo' : 'Failed to create logo'; - + // Handle specific timeout errors if (error.code === 'NETWORK_ERROR' || error.message?.includes('timeout')) { errorMessage = 'Request timed out. Please try again.'; } - + notifications.show({ title: 'Error', message: errorMessage, @@ -85,6 +85,17 @@ const LogoForm = ({ logo = null, isOpen, onClose }) => { if (files.length === 0) return; const file = files[0]; + + // Validate file size on frontend first + if (file.size > 5 * 1024 * 1024) { // 5MB + notifications.show({ + title: 'Error', + message: 'File too large. Maximum size is 5MB.', + color: 'red', + }); + return; + } + setUploading(true); try { @@ -102,12 +113,16 @@ const LogoForm = ({ logo = null, isOpen, onClose }) => { }); } catch (error) { let errorMessage = 'Failed to upload logo'; - + // Handle specific timeout errors if (error.code === 'NETWORK_ERROR' || error.message?.includes('timeout')) { errorMessage = 'Upload timed out. Please try again.'; + } else if (error.status === 413) { + errorMessage = 'File too large. Please choose a smaller file.'; + } else if (error.body?.error) { + errorMessage = error.body.error; } - + notifications.show({ title: 'Error', message: errorMessage,