diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index 50b7309c..73656fb7 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -284,7 +284,10 @@ def process_groups(account, groups): f.regex_pattern, ( re.IGNORECASE - if json.loads(f.custom_properties or "{}").get("case_sensitive", True) == False + if json.loads(f.custom_properties or "{}").get( + "case_sensitive", True + ) + == False else 0 ), ), @@ -525,7 +528,10 @@ def process_m3u_batch(account_id, batch, groups, hash_keys): f.regex_pattern, ( re.IGNORECASE - if json.loads(f.custom_properties or "{}").get("case_sensitive", True) == False + if json.loads(f.custom_properties or "{}").get( + "case_sensitive", True + ) + == False else 0 ), ), @@ -1177,6 +1183,7 @@ def sync_auto_channels(account_id, scan_start_time=None): name_match_regex = None channel_profile_ids = None channel_sort_order = None + channel_sort_reverse = False if group_relation.custom_properties: try: group_custom_props = json.loads(group_relation.custom_properties) @@ -1189,6 +1196,9 @@ def sync_auto_channels(account_id, scan_start_time=None): name_match_regex = group_custom_props.get("name_match_regex") channel_profile_ids = group_custom_props.get("channel_profile_ids") channel_sort_order = group_custom_props.get("channel_sort_order") + channel_sort_reverse = group_custom_props.get( + "channel_sort_reverse", False + ) except Exception: force_dummy_epg = False override_group_id = None @@ -1197,6 +1207,7 @@ def sync_auto_channels(account_id, scan_start_time=None): name_match_regex = None channel_profile_ids = None channel_sort_order = None + channel_sort_reverse = False # Determine which group to use for created channels target_group = channel_group @@ -1240,21 +1251,28 @@ def sync_auto_channels(account_id, scan_start_time=None): # Use natural sorting for names to handle numbers correctly current_streams = list(current_streams) current_streams.sort( - key=lambda stream: natural_sort_key(stream.name) + key=lambda stream: natural_sort_key(stream.name), + reverse=channel_sort_reverse, ) streams_is_list = True elif channel_sort_order == "tvg_id": - current_streams = current_streams.order_by("tvg_id") + order_prefix = "-" if channel_sort_reverse else "" + current_streams = current_streams.order_by(f"{order_prefix}tvg_id") elif channel_sort_order == "updated_at": - current_streams = current_streams.order_by("updated_at") + order_prefix = "-" if channel_sort_reverse else "" + current_streams = current_streams.order_by( + f"{order_prefix}updated_at" + ) else: logger.warning( f"Unknown channel_sort_order '{channel_sort_order}' for group '{channel_group.name}'. Using provider order." ) - current_streams = current_streams.order_by("id") + order_prefix = "-" if channel_sort_reverse else "" + current_streams = current_streams.order_by(f"{order_prefix}id") else: - current_streams = current_streams.order_by("id") - # If channel_sort_order is empty or None, use provider order (no additional sorting) + # Provider order (default) - can still be reversed + order_prefix = "-" if channel_sort_reverse else "" + current_streams = current_streams.order_by(f"{order_prefix}id") # Get existing auto-created channels for this account (regardless of current group) # We'll find them by their stream associations instead of just group location diff --git a/frontend/src/components/forms/M3UGroupFilter.jsx b/frontend/src/components/forms/M3UGroupFilter.jsx index e5918375..64c1d356 100644 --- a/frontend/src/components/forms/M3UGroupFilter.jsx +++ b/frontend/src/components/forms/M3UGroupFilter.jsx @@ -410,8 +410,13 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => { if (newCustomProps.channel_sort_order === undefined) { newCustomProps.channel_sort_order = ''; } + // Keep channel_sort_reverse if it exists + if (newCustomProps.channel_sort_reverse === undefined) { + newCustomProps.channel_sort_reverse = false; + } } else { delete newCustomProps.channel_sort_order; + delete newCustomProps.channel_sort_reverse; // Remove reverse when sort is removed } return { @@ -428,36 +433,65 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => { /> {/* Show only channel_sort_order if selected */} {group.custom_properties?.channel_sort_order !== undefined && ( - { + setGroupStates( + groupStates.map((state) => { + if (state.channel_group === group.channel_group) { + return { + ...state, + custom_properties: { + ...state.custom_properties, + channel_sort_order: value || '', + }, + }; + } + return state; + }) + ); + }} + data={[ + { value: '', label: 'Provider Order (Default)' }, + { value: 'name', label: 'Name' }, + { value: 'tvg_id', label: 'TVG ID' }, + { value: 'updated_at', label: 'Updated At' }, + ]} + clearable + searchable + size="xs" + /> + + {/* Add reverse sort checkbox when sort order is selected (including default) */} + {group.custom_properties?.channel_sort_order !== undefined && ( + + { + setGroupStates( + groupStates.map((state) => { + if (state.channel_group === group.channel_group) { + return { + ...state, + custom_properties: { + ...state.custom_properties, + channel_sort_reverse: event.target.checked, + }, + }; + } + return state; + }) + ); + }} + size="xs" + /> + + )} + )} {/* Show profile selection only if profile_assignment is selected */}