mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-20 16:51:10 +00:00
Lots of status message fixes.
This commit is contained in:
parent
b713b516b4
commit
30e82fb302
5 changed files with 303 additions and 70 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue