Lots of status message fixes.

This commit is contained in:
SergeantPanda 2025-05-04 19:33:03 -05:00
parent b713b516b4
commit 30e82fb302
5 changed files with 303 additions and 70 deletions

View file

@ -386,7 +386,10 @@ def parse_channels_only(source):
# If the source has a URL, fetch the data before continuing
if source.url:
logger.info(f"Fetching new EPG data from URL: {source.url}")
if not fetch_xmltv(source): # Check if fetch was successful
fetch_success = fetch_xmltv(source) # Store the result
# Only proceed if fetch was successful AND file exists
if not fetch_success:
logger.error(f"Failed to fetch EPG data from URL: {source.url}")
# Update status to error
source.status = 'error'
@ -545,15 +548,36 @@ def parse_programs_for_tvg_id(epg_id):
# Fetch new data before continuing
if epg_source.url:
logger.info(f"Fetching new EPG data from URL: {epg_source.url}")
fetch_xmltv(epg_source)
# Properly check the return value from fetch_xmltv
fetch_success = fetch_xmltv(epg_source)
# Check if fetch was successful
# If fetch was not successful or the file still doesn't exist, abort
if not fetch_success:
logger.error(f"Failed to fetch EPG data, cannot parse programs for tvg_id: {epg.tvg_id}")
# Update status to error if not already set
epg_source.status = 'error'
epg_source.last_message = f"Failed to download EPG data, cannot parse programs"
epg_source.save(update_fields=['status', 'last_message'])
send_epg_update(epg_source.id, "parsing_programs", 100, status="error", error="Failed to download EPG file")
release_task_lock('parse_epg_programs', epg_id)
return
# Also check if the file exists after download
if not os.path.exists(new_path):
logger.error(f"Failed to fetch EPG data, file still missing at: {new_path}")
epg_source.status = 'error'
epg_source.last_message = f"Failed to download EPG data, file missing after download"
epg_source.save(update_fields=['status', 'last_message'])
send_epg_update(epg_source.id, "parsing_programs", 100, status="error", error="File not found after download")
release_task_lock('parse_epg_programs', epg_id)
return
else:
logger.error(f"No URL provided for EPG source {epg_source.name}, cannot fetch new data")
# Update status to error
epg_source.status = 'error'
epg_source.last_message = f"No URL provided, cannot fetch EPG data"
epg_source.save(update_fields=['status', 'last_message'])
send_epg_update(epg_source.id, "parsing_programs", 100, status="error", error="No URL provided")
release_task_lock('parse_epg_programs', epg_id)
return

View file

@ -82,36 +82,77 @@ def fetch_m3u_lines(account, use_cache=False):
send_m3u_update(account.id, "downloading", 100)
except Exception as e:
logger.error(f"Error fetching M3U from URL {account.server_url}: {e}")
return []
# Update account status and send error notification
account.status = M3UAccount.Status.ERROR
account.last_message = f"Error downloading M3U file: {str(e)}"
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account.id, "downloading", 100, status="error", error=f"Error downloading M3U file: {str(e)}")
return [], False # Return empty list and False for success
# Check if the file exists and is not empty
if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
error_msg = f"M3U file not found or empty: {file_path}"
logger.error(error_msg)
account.status = M3UAccount.Status.ERROR
account.last_message = error_msg
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account.id, "downloading", 100, status="error", error=error_msg)
return [], False # Return empty list and False for success
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.readlines(), True
except Exception as e:
error_msg = f"Error reading M3U file: {str(e)}"
logger.error(error_msg)
account.status = M3UAccount.Status.ERROR
account.last_message = error_msg
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account.id, "downloading", 100, status="error", error=error_msg)
return [], False
with open(file_path, 'r', encoding='utf-8') as f:
return f.readlines()
elif account.file_path:
try:
if account.file_path.endswith('.gz'):
with gzip.open(account.file_path, 'rt', encoding='utf-8') as f:
return f.readlines()
return f.readlines(), True
elif account.file_path.endswith('.zip'):
with zipfile.ZipFile(account.file_path, 'r') as zip_file:
for name in zip_file.namelist():
if name.endswith('.m3u'):
with zip_file.open(name) as f:
return [line.decode('utf-8') for line in f.readlines()]
logger.warning(f"No .m3u file found in ZIP archive: {account.file_path}")
return []
return [line.decode('utf-8') for line in f.readlines()], True
error_msg = f"No .m3u file found in ZIP archive: {account.file_path}"
logger.warning(error_msg)
account.status = M3UAccount.Status.ERROR
account.last_message = error_msg
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account.id, "downloading", 100, status="error", error=error_msg)
return [], False
else:
with open(account.file_path, 'r', encoding='utf-8') as f:
return f.readlines()
return f.readlines(), True
except (IOError, OSError, zipfile.BadZipFile, gzip.BadGzipFile) as e:
logger.error(f"Error opening file {account.file_path}: {e}")
return []
error_msg = f"Error opening file {account.file_path}: {e}"
logger.error(error_msg)
account.status = M3UAccount.Status.ERROR
account.last_message = error_msg
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account.id, "downloading", 100, status="error", error=error_msg)
return [], False
# Return an empty list if neither server_url nor uploaded_file is available
return []
# Neither server_url nor uploaded_file is available
error_msg = "No M3U source available (missing URL and file)"
logger.error(error_msg)
account.status = M3UAccount.Status.ERROR
account.last_message = error_msg
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account.id, "downloading", 100, status="error", error=error_msg)
return [], False
def parse_extinf_line(line: str) -> dict:
"""
@ -458,6 +499,12 @@ def refresh_m3u_groups(account_id, use_cache=False, full_refresh=False):
try:
xc_client.authenticate()
except Exception as e:
error_msg = f"Failed to authenticate with XC server: {str(e)}"
logger.error(error_msg)
account.status = M3UAccount.Status.ERROR
account.last_message = error_msg
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account_id, "processing_groups", 100, status="error", error=error_msg)
release_task_lock('refresh_m3u_account_groups', account_id)
return f"M3UAccount with ID={account_id} failed to authenticate with XC server.", None
@ -467,7 +514,14 @@ def refresh_m3u_groups(account_id, use_cache=False, full_refresh=False):
"xc_id": category["category_id"],
}
else:
for line in fetch_m3u_lines(account, use_cache):
# Here's the key change - use the success flag from fetch_m3u_lines
lines, success = fetch_m3u_lines(account, use_cache)
if not success:
# If fetch failed, don't continue processing
release_task_lock('refresh_m3u_account_groups', account_id)
return f"Failed to fetch M3U data for account_id={account_id}.", None
for line in lines:
line = line.strip()
if line.startswith("#EXTINF"):
parsed = parse_extinf_line(line)
@ -523,6 +577,7 @@ def refresh_single_m3u_account(account_id):
account = M3UAccount.objects.get(id=account_id, is_active=True)
if not account.is_active:
logger.info(f"Account {account_id} is not active, skipping.")
release_task_lock('refresh_single_m3u_account', account_id)
return
# Set status to fetching
@ -535,7 +590,6 @@ def refresh_single_m3u_account(account_id):
return f"M3UAccount with ID={account_id} not found or inactive."
# Fetch M3U lines and handle potential issues
# lines = fetch_m3u_lines(account) # Extracted fetch logic into separate function
extinf_data = []
groups = None
@ -549,14 +603,45 @@ def refresh_single_m3u_account(account_id):
if not extinf_data:
try:
extinf_data, groups = refresh_m3u_groups(account_id, full_refresh=True)
if not groups:
result = refresh_m3u_groups(account_id, full_refresh=True)
# Check if the result indicates an error (None tuple or tuple with empty values)
if not result or not result[0] or not result[1]:
logger.error(f"Failed to refresh M3U groups for account {account_id}")
# The error already has been recorded by refresh_m3u_groups
# Just release the lock and exit - no need to set parsing status at all
release_task_lock('refresh_single_m3u_account', account_id)
return "Failed to update m3u account, task may already be running"
except:
return "Failed to update m3u account - download failed or other error"
extinf_data, groups = result
if not groups:
logger.error(f"No groups found for account {account_id}")
account.status = M3UAccount.Status.ERROR
account.last_message = "No channel groups found in M3U source"
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account_id, "parsing", 100, status="error", error="No channel groups found")
release_task_lock('refresh_single_m3u_account', account_id)
return "Failed to update m3u account, no groups found"
except Exception as e:
logger.error(f"Exception in refresh_m3u_groups: {str(e)}")
account.status = M3UAccount.Status.ERROR
account.last_message = f"Error refreshing M3U groups: {str(e)}"
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account_id, "parsing", 100, status="error", error=f"Error refreshing M3U groups: {str(e)}")
release_task_lock('refresh_single_m3u_account', account_id)
return "Failed to update m3u account"
# Only proceed with parsing if we actually have data and no errors were encountered
if not extinf_data or not groups:
logger.error(f"No data to process for account {account_id}")
account.status = M3UAccount.Status.ERROR
account.last_message = "No data available for processing"
account.save(update_fields=['status', 'last_message'])
send_m3u_update(account_id, "parsing", 100, status="error", error="No data available for processing")
release_task_lock('refresh_single_m3u_account', account_id)
return "Failed to update m3u account, no data available"
hash_keys = CoreSettings.get_m3u_hash_key().split(",")
existing_groups = {group.name: group.id for group in ChannelGroup.objects.filter(
@ -652,27 +737,11 @@ def refresh_single_m3u_account(account_id):
# Now run cleanup
cleanup_streams(account_id)
# Send final update with complete metrics
elapsed_time = time.time() - start_time
send_m3u_update(
account_id,
"parsing",
100,
elapsed_time=elapsed_time,
time_remaining=0,
streams_processed=streams_processed,
streams_created=streams_created,
streams_updated=streams_updated,
streams_deleted=streams_deleted,
message=account.last_message
)
end_time = time.time()
# Calculate elapsed time
elapsed_time = end_time - start_time
elapsed_time = time.time() - start_time
# Set status to success and update timestamp
# Set status to success and update timestamp BEFORE sending the final update
account.status = M3UAccount.Status.SUCCESS
account.last_message = (
f"Processing completed in {elapsed_time:.1f} seconds. "
@ -682,6 +751,21 @@ def refresh_single_m3u_account(account_id):
account.updated_at = timezone.now()
account.save(update_fields=['status', 'last_message', 'updated_at'])
# Send final update with complete metrics and explicitly include success status
send_m3u_update(
account_id,
"parsing",
100,
status="success", # Explicitly set status to success
elapsed_time=elapsed_time,
time_remaining=0,
streams_processed=streams_processed,
streams_created=streams_created,
streams_updated=streams_updated,
streams_deleted=streams_deleted,
message=account.last_message
)
print(f"Function took {elapsed_time} seconds to execute.")
except Exception as e:

View file

@ -187,7 +187,22 @@ export const WebsocketProvider = ({ children }) => {
break;
case 'm3u_refresh':
// Update the store with progress information
setRefreshProgress(parsedEvent.data);
// If complete (progress is 100%), also update the playlist status
if (parsedEvent.data.progress === 100 && parsedEvent.data.account) {
const playlistsState = usePlaylistsStore.getState();
const playlist = playlistsState.playlists.find(p => p.id === parsedEvent.data.account);
if (playlist) {
playlistsState.updatePlaylist({
...playlist,
status: parsedEvent.data.status || 'success',
last_message: parsedEvent.data.message || playlist.last_message
});
}
}
break;
case 'channel_stats':
@ -272,15 +287,54 @@ export const WebsocketProvider = ({ children }) => {
const epgsState = useEPGsStore.getState();
epgsState.updateEPGProgress(parsedEvent.data);
// If progress is complete (100%), show a notification and refresh EPG data
if (parsedEvent.data.progress === 100 && parsedEvent.data.action === "parsing_programs") {
notifications.show({
title: 'EPG Processing Complete',
message: 'EPG data has been updated successfully',
color: 'green.5',
});
// If we have source_id/account info, update the EPG source status
if (parsedEvent.data.source_id || parsedEvent.data.account) {
const sourceId = parsedEvent.data.source_id || parsedEvent.data.account;
const epg = epgsState.epgs[sourceId];
fetchEPGData();
if (epg) {
// Check for any indication of an error (either via status or error field)
const hasError = parsedEvent.data.status === "error" ||
!!parsedEvent.data.error ||
(parsedEvent.data.message && parsedEvent.data.message.toLowerCase().includes("error"));
if (hasError) {
// Handle error state
const errorMessage = parsedEvent.data.error || parsedEvent.data.message || "Unknown error occurred";
epgsState.updateEPG({
...epg,
status: 'error',
last_message: errorMessage
});
// Show notification for the error
notifications.show({
title: 'EPG Refresh Error',
message: errorMessage,
color: 'red.5',
});
}
// Update status on completion only if no errors
else if (parsedEvent.data.progress === 100) {
epgsState.updateEPG({
...epg,
status: parsedEvent.data.status || 'success',
last_message: parsedEvent.data.message || epg.last_message
});
// Only show success notification if we've finished parsing programs and had no errors
if (parsedEvent.data.action === "parsing_programs") {
notifications.show({
title: 'EPG Processing Complete',
message: 'EPG data has been updated successfully',
color: 'green.5',
});
fetchEPGData();
}
}
}
}
break;

View file

@ -34,8 +34,24 @@ export default function M3URefreshNotification() {
[data.account]: data,
});
// Check for error status FIRST before doing anything else
if (data.status === "error") {
// Only show the error notification if we have a complete task (progress=100)
// or if it's explicitly flagged as an error
if (data.progress === 100) {
notifications.show({
title: `M3U Processing: ${playlist.name}`,
message: `${data.action || 'Processing'} failed: ${data.error || "Unknown error"}`,
color: 'red',
autoClose: 5000, // Keep error visible a bit longer
});
}
return; // Exit early for any error status
}
const taskProgress = data.progress;
// Only show start and completion notifications for normal operation
if (data.progress != 0 && data.progress != 100) {
console.log('not 0 or 100');
return;
@ -61,6 +77,7 @@ export default function M3URefreshNotification() {
} else if (taskProgress == 100) {
message = `${message} complete!`;
// Only trigger additional fetches on successful completion
if (data.action == 'parsing') {
fetchStreams();
} else if (data.action == 'processing_groups') {

View file

@ -14,11 +14,37 @@ import {
ActionIcon,
Tooltip,
Switch,
Progress,
Stack,
} from '@mantine/core';
import { SquareMinus, SquarePen, RefreshCcw, Check, X } from 'lucide-react';
import { IconSquarePlus } from '@tabler/icons-react'; // Import custom icons
import dayjs from 'dayjs';
// Helper function to format status text
const formatStatusText = (status) => {
switch (status) {
case 'idle': return 'Idle';
case 'fetching': return 'Fetching';
case 'parsing': return 'Parsing';
case 'error': return 'Error';
case 'success': return 'Success';
default: return status ? status.charAt(0).toUpperCase() + status.slice(1) : 'Unknown';
}
};
// Helper function to get status text color
const getStatusColor = (status) => {
switch (status) {
case 'idle': return 'gray.5';
case 'fetching': return 'blue.5';
case 'parsing': return 'indigo.5';
case 'error': return 'red.5';
case 'success': return 'green.5';
default: return 'gray.5';
}
};
const M3UTable = () => {
const [playlist, setPlaylist] = useState(null);
const [playlistModalOpen, setPlaylistModalOpen] = useState(false);
@ -258,34 +284,62 @@ const M3UTable = () => {
},
{
header: 'Status',
accessorFn: (row) => {
if (!row.id) {
return '';
}
if (!refreshProgress[row.id]) {
return 'Idle';
accessorKey: 'status',
size: 100,
minSize: 80,
Cell: ({ row }) => {
const data = row.original;
// Check if there's an active progress for this M3U
if (refreshProgress[data.id] && refreshProgress[data.id].progress < 100) {
return generateStatusString(refreshProgress[data.id]);
}
return generateStatusString(refreshProgress[row.id]);
},
size: 180,
minSize: 150,
Cell: ({ cell }) => {
const value = cell.getValue();
// Return a visual component for the status
if (typeof value === 'object') {
return value;
}
// For simple string statuses
// Return simple text display with appropriate color
return (
<Text size="xs">
{value}
<Text
size="sm"
fw={500}
c={getStatusColor(data.status)}
>
{formatStatusText(data.status)}
</Text>
);
},
},
{
header: 'Status Message',
accessorKey: 'last_message',
size: 250,
minSize: 150,
enableSorting: false,
Cell: ({ row }) => {
const data = row.original;
// Show error message when status is error
if (data.status === 'error' && data.last_message) {
return (
<Tooltip label={data.last_message} multiline width={300}>
<Text c="dimmed" size="xs" lineClamp={2} style={{ color: theme.colors.red[6] }}>
{data.last_message}
</Text>
</Tooltip>
);
}
// Show success message for successful sources
if (data.status === 'success' && data.last_message) {
return (
<Text c="dimmed" size="xs" style={{ color: theme.colors.green[6] }}>
{data.last_message}
</Text>
);
}
// Otherwise return empty cell
return null;
},
},
{
header: 'Active',
accessorKey: 'is_active',
@ -313,7 +367,7 @@ const M3UTable = () => {
enableSorting: false,
},
],
[refreshProgress]
[refreshProgress, theme]
);
//optionally access the underlying virtualizer instance