diff --git a/apps/m3u/api_views.py b/apps/m3u/api_views.py index 9c5d5c14..878ae7c6 100644 --- a/apps/m3u/api_views.py +++ b/apps/m3u/api_views.py @@ -81,6 +81,13 @@ class M3UAccountViewSet(viewsets.ModelViewSet): account_type = response.data.get("account_type") account_id = response.data.get("id") + # Notify frontend that a new playlist was created + from core.utils import send_websocket_update + send_websocket_update('updates', 'update', { + 'type': 'playlist_created', + 'playlist_id': account_id + }) + if account_type == M3UAccount.Types.XC: refresh_m3u_groups(account_id) diff --git a/frontend/src/WebSocket.jsx b/frontend/src/WebSocket.jsx index 1101c9f8..eaa90812 100644 --- a/frontend/src/WebSocket.jsx +++ b/frontend/src/WebSocket.jsx @@ -36,7 +36,6 @@ export const WebsocketProvider = ({ children }) => { const updateEPG = useEPGsStore((s) => s.updateEPG); const updateEPGProgress = useEPGsStore((s) => s.updateEPGProgress); - const playlists = usePlaylistsStore((s) => s.playlists); const updatePlaylist = usePlaylistsStore((s) => s.updatePlaylist); // Calculate reconnection delay with exponential backoff @@ -247,10 +246,14 @@ export const WebsocketProvider = ({ children }) => { // Update the playlist status whenever we receive a status update // Not just when progress is 100% or status is pending_setup if (parsedEvent.data.status && parsedEvent.data.account) { - // Check if playlists is an object with IDs as keys or an array - const playlist = Array.isArray(playlists) - ? playlists.find((p) => p.id === parsedEvent.data.account) - : playlists[parsedEvent.data.account]; + // Get fresh playlists from store to avoid stale state from React render cycle + const currentPlaylists = usePlaylistsStore.getState().playlists; + const isArray = Array.isArray(currentPlaylists); + const playlist = isArray + ? currentPlaylists.find( + (p) => p.id === parsedEvent.data.account + ) + : currentPlaylists[parsedEvent.data.account]; if (playlist) { // When we receive a "success" status with 100% progress, this is a completed refresh @@ -279,13 +282,13 @@ export const WebsocketProvider = ({ children }) => { fetchPlaylists(); // Refresh playlists to ensure UI is up-to-date fetchChannelProfiles(); // Ensure channel profiles are updated } else { - // Log when playlist can't be found for debugging purposes - console.warn( - `Received update for unknown playlist ID: ${parsedEvent.data.account}`, - Array.isArray(playlists) - ? 'playlists is array' - : 'playlists is object', - Object.keys(playlists).length + // Playlist not in store yet - this happens when backend sends websocket + // updates immediately after creating the playlist, before the API response + // returns. The frontend will receive a 'playlist_created' event shortly + // which will trigger a fetchPlaylists() to sync the store. + console.log( + `Received update for playlist ID ${parsedEvent.data.account} not yet in store. ` + + `Waiting for playlist_created event to sync...` ); } } @@ -739,6 +742,14 @@ export const WebsocketProvider = ({ children }) => { break; + case 'playlist_created': + // Backend signals that a new playlist has been created and we should refresh + console.log( + 'Playlist created event received, refreshing playlists...' + ); + fetchPlaylists(); + break; + case 'bulk_channel_creation_progress': { // Handle progress updates with persistent notifications like stream rehash const data = parsedEvent.data; diff --git a/frontend/src/components/forms/M3U.jsx b/frontend/src/components/forms/M3U.jsx index 2da4d75a..93581408 100644 --- a/frontend/src/components/forms/M3U.jsx +++ b/frontend/src/components/forms/M3U.jsx @@ -23,7 +23,6 @@ import { } from '@mantine/core'; import M3UGroupFilter from './M3UGroupFilter'; import useChannelsStore from '../../store/channels'; -import usePlaylistsStore from '../../store/playlists'; import { notifications } from '@mantine/notifications'; import { isNotEmpty, useForm } from '@mantine/form'; import useEPGsStore from '../../store/epgs'; @@ -40,7 +39,6 @@ const M3U = ({ const userAgents = useUserAgentsStore((s) => s.userAgents); const fetchChannelGroups = useChannelsStore((s) => s.fetchChannelGroups); - const fetchPlaylists = usePlaylistsStore((s) => s.fetchPlaylists); const fetchEPGs = useEPGsStore((s) => s.fetchEPGs); const fetchCategories = useVODStore((s) => s.fetchCategories); @@ -171,8 +169,14 @@ const M3U = ({ return; } + // Fetch the updated playlist details (this also updates the store via API) const updatedPlaylist = await API.getPlaylist(newPlaylist.id); - await Promise.all([fetchChannelGroups(), fetchPlaylists(), fetchEPGs()]); + + // Note: We don't call fetchPlaylists() here because API.addPlaylist() + // already added the playlist to the store. Calling fetchPlaylists() creates + // a race condition where the store is temporarily cleared/replaced while + // websocket updates for the new playlist's refresh task are arriving. + await Promise.all([fetchChannelGroups(), fetchEPGs()]); // If this is an XC account with VOD enabled, also fetch VOD categories if (values.account_type === 'XC' && values.enable_vod) {