Merge branch 'dev' into cron-scheduling

This commit is contained in:
SergeantPanda 2026-02-16 10:55:33 -06:00 committed by GitHub
commit 717cce92ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 190 additions and 33 deletions

View file

@ -10,9 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Cron scheduling support for M3U and EPG refreshes: Added interactive cron expression builder with preset buttons and custom field editors, plus info popover with common cron examples. Refactored backup scheduling to use shared ScheduleInput component for consistency across all scheduling interfaces. (Closes #165)
- Channel numbering modes for auto channel sync: Added three channel numbering modes when auto-syncing channels from M3U groups:
- **Fixed Start Number** (default): Start at a specified number and increment sequentially
- **Use Provider Number**: Use channel numbers from the M3U source (tvg-chno), with configurable fallback if provider number is missing
- **Next Available**: Auto-assign starting from 1, skipping all used channel numbers
Each mode includes its own configuration options accessible via the "Channel Numbering Mode" dropdown in auto sync settings. (Closes #956, #433)
### Fixed
- Auto channel sync duplicate channel numbers across groups: Fixed issue where multiple auto-sync groups starting at the same number would create duplicate channel numbers. The used channel number tracking now persists across all groups in a single sync operation, ensuring each assigned channel number is globally unique.
- Modular mode PostgreSQL/Redis connection checks: Replaced raw Python socket checks with native tools (`pg_isready` for PostgreSQL and `socket.create_connection` for Redis) in modular deployment mode to prevent indefinite hangs in Docker environments with non-standard networking or DNS configurations. Now properly supports IPv4 and IPv6 configurations. (Fixes #952) - Thanks [@CodeBormen](https://github.com/CodeBormen)
## [0.19.0] - 2026-02-10

View file

@ -1648,6 +1648,13 @@ def sync_auto_channels(account_id, scan_start_time=None):
channels_updated = 0
channels_deleted = 0
# Get all channel numbers that are already in use by other channels (not auto-created by this account)
used_numbers = set(
Channel.objects.exclude(
auto_created=True, auto_created_by=account
).values_list("channel_number", flat=True)
)
for group_relation in auto_sync_groups:
channel_group = group_relation.channel_group
start_number = group_relation.auto_sync_channel_start or 1.0
@ -1665,6 +1672,8 @@ def sync_auto_channels(account_id, scan_start_time=None):
stream_profile_id = None
custom_logo_id = None
custom_epg_id = None # New option: select specific EPG source (takes priority over force_dummy_epg)
channel_numbering_mode = "fixed" # Default mode
channel_numbering_fallback = 1 # Default fallback for provider mode
if group_relation.custom_properties:
group_custom_props = group_relation.custom_properties
force_dummy_epg = group_custom_props.get("force_dummy_epg", False)
@ -1682,6 +1691,8 @@ def sync_auto_channels(account_id, scan_start_time=None):
)
stream_profile_id = group_custom_props.get("stream_profile_id")
custom_logo_id = group_custom_props.get("custom_logo_id")
channel_numbering_mode = group_custom_props.get("channel_numbering_mode", "fixed")
channel_numbering_fallback = group_custom_props.get("channel_numbering_fallback", 1)
# Determine which group to use for created channels
target_group = channel_group
@ -1697,7 +1708,7 @@ def sync_auto_channels(account_id, scan_start_time=None):
)
logger.info(
f"Processing auto sync for group: {channel_group.name} (start: {start_number})"
f"Processing auto sync for group: {channel_group.name} (mode: {channel_numbering_mode}, start: {start_number})"
)
# Get all current streams in this group for this M3U account, filter out stale streams
@ -1837,21 +1848,35 @@ def sync_auto_channels(account_id, scan_start_time=None):
channels_to_renumber = []
temp_channel_number = start_number
# Get all channel numbers that are already in use by other channels (not auto-created by this account)
used_numbers = set(
Channel.objects.exclude(
auto_created=True, auto_created_by=account
).values_list("channel_number", flat=True)
)
for stream in current_streams:
if stream.id in existing_channel_map:
channel = existing_channel_map[stream.id]
# Find next available number starting from temp_channel_number
target_number = temp_channel_number
while target_number in used_numbers:
target_number += 1
# Determine target number based on numbering mode
if channel_numbering_mode == "provider":
# Use provider number if available, otherwise use fallback with next available logic
if stream.stream_chno is not None:
target_number = stream.stream_chno
# If provider number is already used, find next available
if target_number in used_numbers:
target_number = channel_numbering_fallback
while target_number in used_numbers:
target_number += 1
else:
# No provider number, use fallback and find next available
target_number = channel_numbering_fallback
while target_number in used_numbers:
target_number += 1
elif channel_numbering_mode == "next_available":
# Find next available starting from 1
target_number = 1
while target_number in used_numbers:
target_number += 1
else: # fixed mode (default)
# Find next available number starting from temp_channel_number
target_number = temp_channel_number
while target_number in used_numbers:
target_number += 1
# Add this number to used_numbers so we don't reuse it in this batch
used_numbers.add(target_number)
@ -1863,9 +1888,11 @@ def sync_auto_channels(account_id, scan_start_time=None):
f"Will renumber channel '{channel.name}' to {target_number}"
)
temp_channel_number += 1.0
if temp_channel_number % 1 != 0: # Has decimal
temp_channel_number = int(temp_channel_number) + 1.0
# Only increment temp_channel_number in fixed mode
if channel_numbering_mode == "fixed":
temp_channel_number += 1.0
if temp_channel_number % 1 != 0: # Has decimal
temp_channel_number = int(temp_channel_number) + 1.0
# Bulk update channel numbers if any need renumbering
if channels_to_renumber:
@ -2060,10 +2087,31 @@ def sync_auto_channels(account_id, scan_start_time=None):
else:
# Create new channel
# Find next available channel number
target_number = current_channel_number
while target_number in used_numbers:
target_number += 1
# Determine channel number based on numbering mode
if channel_numbering_mode == "provider":
# Use provider number if available, otherwise use fallback with next available logic
if stream.stream_chno is not None:
target_number = stream.stream_chno
# If provider number is already used, find next available from fallback
if target_number in used_numbers:
target_number = channel_numbering_fallback
while target_number in used_numbers:
target_number += 1
else:
# No provider number, use fallback and find next available
target_number = channel_numbering_fallback
while target_number in used_numbers:
target_number += 1
elif channel_numbering_mode == "next_available":
# Find next available starting from 1
target_number = 1
while target_number in used_numbers:
target_number += 1
else: # fixed mode (default)
# Find next available channel number starting from current_channel_number
target_number = current_channel_number
while target_number in used_numbers:
target_number += 1
# Add this number to used_numbers
used_numbers.add(target_number)
@ -2190,10 +2238,11 @@ def sync_auto_channels(account_id, scan_start_time=None):
f"Created auto channel: {channel.channel_number} - {channel.name}"
)
# Increment channel number for next iteration
current_channel_number += 1.0
if current_channel_number % 1 != 0: # Has decimal
current_channel_number = int(current_channel_number) + 1.0
# Increment channel number for next iteration (only in fixed mode)
if channel_numbering_mode == "fixed":
current_channel_number += 1.0
if current_channel_number % 1 != 0: # Has decimal
current_channel_number = int(current_channel_number) + 1.0
except Exception as e:
logger.error(

View file

@ -314,17 +314,119 @@ const LiveGroupFilter = ({
{group.auto_channel_sync && group.enabled && (
<>
<NumberInput
label="Start Channel #"
value={group.auto_sync_channel_start}
onChange={(value) =>
updateChannelStart(group.channel_group, value)
<Tooltip
label={
<div>
<div>
<strong>Fixed:</strong> Start at a specific number
and increment
</div>
<div>
<strong>Provider:</strong> Use channel numbers
from the M3U source
</div>
<div>
<strong>Next Available:</strong> Auto-assign
starting from 1, skipping used numbers
</div>
</div>
}
min={1}
step={1}
size="xs"
precision={1}
/>
withArrow
multiline
w={280}
openDelay={500}
>
<Select
label="Channel Numbering Mode"
placeholder="Select mode..."
value={
group.custom_properties?.channel_numbering_mode ||
'fixed'
}
onChange={(value) => {
setGroupStates(
groupStates.map((state) => {
if (
state.channel_group === group.channel_group
) {
return {
...state,
custom_properties: {
...state.custom_properties,
channel_numbering_mode: value || 'fixed',
},
};
}
return state;
})
);
}}
data={[
{
value: 'fixed',
label: 'Fixed Start Number',
},
{
value: 'provider',
label: 'Use Provider Number',
},
{
value: 'next_available',
label: 'Next Available',
},
]}
size="xs"
/>
</Tooltip>
{(!group.custom_properties?.channel_numbering_mode ||
group.custom_properties?.channel_numbering_mode ===
'fixed') && (
<NumberInput
label="Start Channel #"
value={group.auto_sync_channel_start}
onChange={(value) =>
updateChannelStart(group.channel_group, value)
}
min={1}
step={1}
size="xs"
precision={0}
/>
)}
{group.custom_properties?.channel_numbering_mode ===
'provider' && (
<NumberInput
label="Fallback Channel # (if provider # missing)"
value={
group.custom_properties
?.channel_numbering_fallback || 1
}
onChange={(value) => {
setGroupStates(
groupStates.map((state) => {
if (
state.channel_group === group.channel_group
) {
return {
...state,
custom_properties: {
...state.custom_properties,
channel_numbering_fallback: value || 1,
},
};
}
return state;
})
);
}}
min={1}
step={1}
size="xs"
precision={0}
/>
)}
{/* Auto Channel Sync Options Multi-Select */}
<MultiSelect