diff --git a/apps/m3u/signals.py b/apps/m3u/signals.py index c2abc4c8..c07c1c4c 100644 --- a/apps/m3u/signals.py +++ b/apps/m3u/signals.py @@ -12,4 +12,4 @@ def refresh_account_on_save(sender, instance, created, **kwargs): if it is active or newly created. """ if created: - refresh_m3u_groups(instance.id) + refresh_single_m3u_account.delay(instance.id) diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index e0995543..82f2310b 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -8,6 +8,7 @@ from celery.result import AsyncResult from celery import shared_task, current_app, group from django.conf import settings from django.core.cache import cache +from django.db import transaction from .models import M3UAccount from apps.channels.models import Stream, ChannelGroup, ChannelGroupM3UAccount from asgiref.sync import async_to_sync @@ -179,7 +180,7 @@ def process_m3u_batch(account_id, batch, group_names, hash_keys): account = M3UAccount.objects.get(id=account_id) existing_groups = {group.name: group for group in ChannelGroup.objects.filter( m3u_account__m3u_account=account, # Filter by the M3UAccount - m3u_account__enabled=True # Filter by the enabled flag in the join table + m3u_account__enabled=True # Filter by the enabled flag in the join table )} streams_to_create = [] @@ -269,9 +270,29 @@ def process_m3u_batch(account_id, batch, group_names, hash_keys): return f"Batch processed: {len(streams_to_create)} created, {len(streams_to_update)} updated." +def cleanup_streams(account_id): + account = M3UAccount.objects.get(id=account_id, is_active=True) + existing_groups = ChannelGroup.objects.filter( + m3u_account__m3u_account=account, + m3u_account__enabled=True, + ).values_list('id', flat=True) + logger.info(f"Found {len(existing_groups)} active groups") + streams = Stream.objects.filter(m3u_account=account) + + streams_to_delete = Stream.objects.filter( + m3u_account=account + ).exclude( + channel_group__in=existing_groups # Exclude products having any of the excluded tags + ) + + # Delete the filtered products + streams_to_delete.delete() + + logger.info(f"Cleanup complete") + def refresh_m3u_groups(account_id): if not acquire_lock('refresh_m3u_account_groups', account_id): - return f"Task already running for account_id={account_id}." + return f"Task already running for account_id={account_id}.", None # Record start time start_time = time.time() @@ -345,7 +366,10 @@ def refresh_single_m3u_account(account_id, use_cache=False): groups = data['groups'] if not extinf_data: - extinf_data, groups = refresh_m3u_groups(account_id) + try: + extinf_data, groups = refresh_m3u_groups(account_id) + except: + return "Failed to update m3u account" hash_keys = CoreSettings.get_m3u_hash_key().split(",") @@ -371,6 +395,10 @@ def refresh_single_m3u_account(account_id, use_cache=False): progress = int((completed_batches / total_batches) * 100) # Send progress update via Channels + # Don't send 100% because we want to clean up after + if progress == 100: + progress = 99 + send_progress_update(progress, account_id) # Optionally remove completed task from the group to prevent processing it again @@ -378,6 +406,10 @@ def refresh_single_m3u_account(account_id, use_cache=False): else: logger.debug(f"Task is still running.") + # Run cleanup + cleanup_streams(account_id) + send_progress_update(100, account_id) + end_time = time.time() # Calculate elapsed time diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d4ccfe78..1b91bb9b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,10 +8,11 @@ "name": "vite", "version": "0.0.0", "dependencies": { - "@mantine/core": "^7.17.0", - "@mantine/dates": "^7.17.0", - "@mantine/hooks": "^7.17.0", - "@mantine/notifications": "^7.17.1", + "@mantine/core": "^7.17.2", + "@mantine/dates": "^7.17.2", + "@mantine/dropzone": "^7.17.2", + "@mantine/hooks": "^7.17.2", + "@mantine/notifications": "^7.17.2", "@tabler/icons-react": "^3.31.0", "allotment": "^1.20.3", "axios": "^1.8.2", @@ -1082,9 +1083,9 @@ "license": "Apache-2.0" }, "node_modules/@mantine/core": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.17.1.tgz", - "integrity": "sha512-V8O3Ftq4la4I4wNDkTfH4Slkt/pCEU32pTE/DkO46zua0VFxfOAJeLjaol0s11//T+bXx82DtjMsd9APWPuFhA==", + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.17.2.tgz", + "integrity": "sha512-R6MYhitJ0JEgrhadd31Nw9FhRaQwDHjXUs5YIlitKH/fTOz9gKSxKjzmNng3bEBQCcbEDOkZj3FRcBgTUh/F0Q==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.28", @@ -1095,56 +1096,71 @@ "type-fest": "^4.27.0" }, "peerDependencies": { - "@mantine/hooks": "7.17.1", + "@mantine/hooks": "7.17.2", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/dates": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.17.1.tgz", - "integrity": "sha512-L9MlIDb528RpznUeeW71xS4q3lYGolElz/f7xGRXEu9gHLaNJufbxroTw2N8RC6p/+RN1ZrSXEsjlr2euiofAw==", + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.17.2.tgz", + "integrity": "sha512-7bB992j8f+uEi280jab0/8i5yfsN/3oSrMDFwatZ+7XSDUwiP0YFib/FVX0pNSSqdFpbXhUmsZEECX71QtHw+Q==", "license": "MIT", "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { - "@mantine/core": "7.17.1", - "@mantine/hooks": "7.17.1", + "@mantine/core": "7.17.2", + "@mantine/hooks": "7.17.2", "dayjs": ">=1.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, + "node_modules/@mantine/dropzone": { + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.17.2.tgz", + "integrity": "sha512-NMQ1SDmnW0sf3GO6p1r/VIcg/xWqlRmfnWCr00/bGRbBEGbyaUwL3LSn+KYBJdY+3/jNGvGa+xflWDvnby5tzw==", + "license": "MIT", + "dependencies": { + "react-dropzone-esm": "15.2.0" + }, + "peerDependencies": { + "@mantine/core": "7.17.2", + "@mantine/hooks": "7.17.2", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, "node_modules/@mantine/hooks": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.17.1.tgz", - "integrity": "sha512-mkHLrXMPd5xdI5WD7UOLwNEpdh/i6A7HaRDTXvjDE2/S0N8VmAE+BlvdyvWRMi7ODp2zVqJdP8cF1tgUn+Z0fA==", + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.17.2.tgz", + "integrity": "sha512-tbErVcGZu0E4dSmE6N0k6Tv1y9R3SQmmQgwqorcc+guEgKMdamc36lucZGlJnSGUmGj+WLUgELkEQ0asdfYBDA==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" } }, "node_modules/@mantine/notifications": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.17.1.tgz", - "integrity": "sha512-jsCNkkjgtsGYIMbCrzBY0UBckoXyeaSWbEoJdvMlfA+LaeOQrSLxa+ot+1+wPaoZxR+1Q1xOwC1X5bTxHKudBA==", + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.17.2.tgz", + "integrity": "sha512-vg0L8cmihz0ODg4WJ9MAyK06WPt/6g67ksIUFxd4F8RfdJbIMLTsNG9yWoSfuhtXenUg717KaA917IWLjDSaqw==", "license": "MIT", "dependencies": { - "@mantine/store": "7.17.1", + "@mantine/store": "7.17.2", "react-transition-group": "4.4.5" }, "peerDependencies": { - "@mantine/core": "7.17.1", - "@mantine/hooks": "7.17.1", + "@mantine/core": "7.17.2", + "@mantine/hooks": "7.17.2", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/store": { - "version": "7.17.1", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.17.1.tgz", - "integrity": "sha512-is1c0FycakMsbTElKGWO59LarjMIk24JUXfjP/QIrB0XqpXreq3u7aN4hoNqr1IftTZSfVBii4W8pVFeWaV55g==", + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.17.2.tgz", + "integrity": "sha512-UoMUYQK/z58hMueCkpDIXc49gPgrVO/zcpb0k+B7MFU51EIUiFzHLxLFBmWrgCAM6rzJORqN8JjyCd/PB9j4aw==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" @@ -3603,6 +3619,21 @@ "node": ">=6" } }, + "node_modules/react-dropzone-esm": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/react-dropzone-esm/-/react-dropzone-esm-15.2.0.tgz", + "integrity": "sha512-pPwR8xWVL+tFLnbAb8KVH5f6Vtl397tck8dINkZ1cPMxHWH+l9dFmIgRWgbh7V7jbjIcuKXCsVrXbhQz68+dVA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-fast-compare": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 26e99b33..de53fcc4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,10 +10,11 @@ "preview": "vite preview" }, "dependencies": { - "@mantine/core": "^7.17.0", - "@mantine/dates": "^7.17.0", - "@mantine/hooks": "^7.17.0", - "@mantine/notifications": "^7.17.1", + "@mantine/core": "^7.17.2", + "@mantine/dates": "^7.17.2", + "@mantine/dropzone": "^7.17.2", + "@mantine/hooks": "^7.17.2", + "@mantine/notifications": "^7.17.2", "@tabler/icons-react": "^3.31.0", "allotment": "^1.20.3", "axios": "^1.8.2", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index bfe43fc1..c769dcc6 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -22,6 +22,7 @@ import { Box, AppShell, MantineProvider } from '@mantine/core'; import '@mantine/core/styles.css'; // Ensure Mantine global styles load import '@mantine/notifications/styles.css'; import 'mantine-react-table/styles.css'; +import '@mantine/dropzone/styles.css'; import './index.css'; import mantineTheme from './mantineTheme'; import API from './api'; diff --git a/frontend/src/WebSocket.jsx b/frontend/src/WebSocket.jsx index 0538887d..bd9f0e5d 100644 --- a/frontend/src/WebSocket.jsx +++ b/frontend/src/WebSocket.jsx @@ -19,7 +19,7 @@ export const WebsocketProvider = ({ children }) => { const { fetchStreams } = useStreamsStore(); const { setChannelStats, fetchChannelGroups } = useChannelsStore(); - const { setRefreshProgress } = usePlaylistsStore(); + const { fetchPlaylists, setRefreshProgress } = usePlaylistsStore(); const { fetchEPGData } = useEPGsStore(); const ws = useRef(null); @@ -68,6 +68,7 @@ export const WebsocketProvider = ({ children }) => { fetchStreams(); fetchChannelGroups(); fetchEPGData(); + fetchPlaylists(); } setRefreshProgress(event.data.account, event.data.progress); } diff --git a/frontend/src/api.js b/frontend/src/api.js index 0590e2d4..369c49c2 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -197,13 +197,33 @@ export default class API { static async updateChannel(values) { const { id, ...payload } = values; + + let body = null; + if (values.logo_file) { + // Must send FormData for file upload + body = new FormData(); + for (const prop in values) { + body.append(prop, values[prop]); + } + } else { + body = { ...values }; + delete body.logo_file; + body = JSON.stringify(body); + } + + console.log(body); + const response = await fetch(`${host}/api/channels/channels/${id}/`, { method: 'PUT', headers: { Authorization: `Bearer ${await API.getAuthToken()}`, - 'Content-Type': 'application/json', + ...(values.logo_file + ? {} + : { + 'Content-Type': 'application/json', + }), }, - body: JSON.stringify(payload), + body: body, }); const retval = await response.json(); diff --git a/frontend/src/components/FloatingVideo.jsx b/frontend/src/components/FloatingVideo.jsx index 62e53e3e..451df5cd 100644 --- a/frontend/src/components/FloatingVideo.jsx +++ b/frontend/src/components/FloatingVideo.jsx @@ -3,8 +3,7 @@ import React, { useEffect, useRef } from 'react'; import Draggable from 'react-draggable'; import useVideoStore from '../store/useVideoStore'; import mpegts from 'mpegts.js'; -import { ActionIcon, Flex } from '@mantine/core'; -import { SquareX } from 'lucide-react'; +import { CloseButton, Flex } from '@mantine/core'; export default function FloatingVideo() { const { isVisible, streamUrl, hideVideo } = useVideoStore(); @@ -68,9 +67,7 @@ export default function FloatingVideo() { > {/* Simple header row with a close button */} - - - + {/* The