New feature: Allow specifying channel number during channel creation.

Closes #377
Closes #169 [Feature]: Starting channel number/continue auto numbering from channel number X (user defined)
This commit is contained in:
SergeantPanda 2025-09-14 12:46:30 -05:00
parent 0411fe003a
commit 4886426ea0
2 changed files with 144 additions and 7 deletions

View file

@ -1604,13 +1604,13 @@ def bulk_create_channels_from_streams(self, stream_ids, channel_profile_ids=None
"""
Asynchronously create channels from a list of stream IDs.
Provides progress updates via WebSocket.
Args:
stream_ids: List of stream IDs to create channels from
channel_profile_ids: Optional list of channel profile IDs to assign channels to
starting_channel_number: Optional starting channel number behavior:
- None: Use provider channel numbers, then auto-assign from 1
- 0: Start with lowest available number and increment by 1
- 0: Start with lowest available number and increment by 1
- Other number: Use as starting number for auto-assignment
"""
from apps.channels.models import Stream, Channel, ChannelGroup, ChannelProfile, ChannelProfileMembership, Logo
@ -1637,7 +1637,7 @@ def bulk_create_channels_from_streams(self, stream_ids, channel_profile_ids=None
# Gather current used numbers once
used_numbers = set(Channel.objects.all().values_list("channel_number", flat=True))
# Initialize next_number based on starting_channel_number mode
if starting_channel_number is None:
# Mode 1: Use provider numbers when available, auto-assign when not
@ -1688,7 +1688,7 @@ def bulk_create_channels_from_streams(self, stream_ids, channel_profile_ids=None
# Determine channel number based on starting_channel_number mode
channel_number = None
if starting_channel_number is None:
# Mode 1: Use provider numbers when available
if "tvg-chno" in stream_custom_props:
@ -1697,7 +1697,7 @@ def bulk_create_channels_from_streams(self, stream_ids, channel_profile_ids=None
channel_number = float(stream_custom_props["channel-number"])
elif "num" in stream_custom_props:
channel_number = float(stream_custom_props["num"])
# For modes 2 and 3 (starting_channel_number == 0 or specific number),
# ignore provider numbers and use sequential assignment

View file

@ -39,11 +39,16 @@ import {
UnstyledButton,
LoadingOverlay,
Skeleton,
Modal,
NumberInput,
Radio,
Checkbox,
} from '@mantine/core';
import { useNavigate } from 'react-router-dom';
import useSettingsStore from '../../store/settings';
import useVideoStore from '../../store/useVideoStore';
import useChannelsTableStore from '../../store/channelsTable';
import useWarningsStore from '../../store/warnings';
import { CustomTable, useTable } from './CustomTable';
import useLocalStorage from '../../hooks/useLocalStorage';
@ -197,6 +202,13 @@ const StreamsTable = () => {
const [sorting, setSorting] = useState([{ id: 'name', desc: '' }]);
const [selectedStreamIds, setSelectedStreamIds] = useState([]);
// Channel numbering modal state
const [channelNumberingModalOpen, setChannelNumberingModalOpen] =
useState(false);
const [numberingMode, setNumberingMode] = useState('provider'); // 'provider', 'auto', or 'custom'
const [customStartNumber, setCustomStartNumber] = useState(1);
const [rememberChoice, setRememberChoice] = useState(false);
// const [allRowsSelected, setAllRowsSelected] = useState(false);
// Add local storage for page size
@ -249,6 +261,10 @@ const StreamsTable = () => {
const showVideo = useVideoStore((s) => s.showVideo);
const [tableSize, _] = useLocalStorage('table-size', 'default');
// Warnings store for "remember choice" functionality
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
const handleSelectClick = (e) => {
e.stopPropagation();
e.preventDefault();
@ -419,11 +435,35 @@ const StreamsTable = () => {
fetchChannelGroups,
]);
// Bulk creation: create channels from selected streams in one API call
// Bulk creation: create channels from selected streams asynchronously
const createChannelsFromStreams = async () => {
if (selectedStreamIds.length === 0) return;
// Check if user has suppressed the channel numbering dialog
const actionKey = 'channel-numbering-choice';
if (isWarningSuppressed(actionKey)) {
// Use the remembered settings or default to 'provider' mode
const savedMode =
localStorage.getItem('channel-numbering-mode') || 'provider';
const savedStartNumber =
localStorage.getItem('channel-numbering-start') || '1';
const startingChannelNumberValue =
savedMode === 'provider'
? null
: savedMode === 'auto'
? 0
: Number(savedStartNumber);
await executeChannelCreation(startingChannelNumberValue);
} else {
// Show the modal to let user choose
setChannelNumberingModalOpen(true);
}
};
// Separate function to actually execute the channel creation
const executeChannelCreation = async (startingChannelNumberValue) => {
try {
const selectedChannelProfileId =
useChannelsStore.getState().selectedProfileId;
@ -431,7 +471,8 @@ const StreamsTable = () => {
// Use the async API for all bulk operations
const response = await API.createChannelsFromStreamsAsync(
selectedStreamIds,
selectedChannelProfileId !== '0' ? [selectedChannelProfileId] : null
selectedChannelProfileId !== '0' ? [selectedChannelProfileId] : null,
startingChannelNumberValue
);
console.log(
@ -446,6 +487,32 @@ const StreamsTable = () => {
}
};
// Handle confirming the channel numbering modal
const handleChannelNumberingConfirm = async () => {
// Save the choice if user wants to remember it
if (rememberChoice) {
suppressWarning('channel-numbering-choice');
localStorage.setItem('channel-numbering-mode', numberingMode);
if (numberingMode === 'custom') {
localStorage.setItem(
'channel-numbering-start',
customStartNumber.toString()
);
}
}
// Convert mode to API value
const startingChannelNumberValue =
numberingMode === 'provider'
? null
: numberingMode === 'auto'
? 0
: Number(customStartNumber);
setChannelNumberingModalOpen(false);
await executeChannelCreation(startingChannelNumberValue);
};
const editStream = async (stream = null) => {
setStream(stream);
setModalOpen(true);
@ -900,6 +967,76 @@ const StreamsTable = () => {
isOpen={modalOpen}
onClose={closeStreamForm}
/>
{/* Channel Numbering Modal */}
<Modal
opened={channelNumberingModalOpen}
onClose={() => setChannelNumberingModalOpen(false)}
title="Channel Numbering Options"
size="md"
centered
>
<Stack spacing="md">
<Text size="sm" c="dimmed">
Choose how to assign channel numbers to the{' '}
{selectedStreamIds.length} selected streams:
</Text>
<Radio.Group
value={numberingMode}
onChange={setNumberingMode}
label="Numbering Mode"
>
<Stack mt="xs" spacing="xs">
<Radio
value="provider"
label="Use Provider Numbers"
description="Use tvg-chno or channel-number from stream metadata, auto-assign for conflicts"
/>
<Radio
value="auto"
label="Auto-Assign Sequential"
description="Start from the lowest available channel number and increment by 1"
/>
<Radio
value="custom"
label="Start from Custom Number"
description="Start sequential numbering from a specific channel number"
/>
</Stack>
</Radio.Group>
{numberingMode === 'custom' && (
<NumberInput
label="Starting Channel Number"
description="Channel numbers will be assigned starting from this number"
value={customStartNumber}
onChange={setCustomStartNumber}
min={1}
max={9999}
placeholder="Enter starting number..."
/>
)}
<Checkbox
checked={rememberChoice}
onChange={(event) => setRememberChoice(event.currentTarget.checked)}
label="Remember this choice and don't ask again"
/>
<Group justify="flex-end" mt="md">
<Button
variant="default"
onClick={() => setChannelNumberingModalOpen(false)}
>
Cancel
</Button>
<Button onClick={handleChannelNumberingConfirm}>
Create Channels
</Button>
</Group>
</Stack>
</Modal>
</>
);
};