mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
Merge branch 'dev' into cron-scheduling
This commit is contained in:
commit
717cce92ec
3 changed files with 190 additions and 33 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue