mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-08-04 07:42:47 +00:00
Merge branch 'dev' of https://github.com/Dispatcharr/Dispatcharr into logo-manager
This commit is contained in:
commit
122b902f0f
6 changed files with 216 additions and 43 deletions
|
|
@ -321,13 +321,34 @@ class ChannelGroupM3UAccountSerializer(serializers.ModelSerializer):
|
|||
enabled = serializers.BooleanField()
|
||||
auto_channel_sync = serializers.BooleanField(default=False)
|
||||
auto_sync_channel_start = serializers.FloatField(allow_null=True, required=False)
|
||||
custom_properties = serializers.JSONField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = ChannelGroupM3UAccount
|
||||
fields = ["id", "channel_group", "enabled", "auto_channel_sync", "auto_sync_channel_start"]
|
||||
fields = ["id", "channel_group", "enabled", "auto_channel_sync", "auto_sync_channel_start", "custom_properties"]
|
||||
|
||||
# Optionally, if you only need the id of the ChannelGroup, you can customize it like this:
|
||||
# channel_group = serializers.PrimaryKeyRelatedField(queryset=ChannelGroup.objects.all())
|
||||
def to_representation(self, instance):
|
||||
ret = super().to_representation(instance)
|
||||
# Ensure custom_properties is always a dict or None
|
||||
val = ret.get("custom_properties")
|
||||
if isinstance(val, str):
|
||||
import json
|
||||
try:
|
||||
ret["custom_properties"] = json.loads(val)
|
||||
except Exception:
|
||||
ret["custom_properties"] = None
|
||||
return ret
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# Accept both dict and JSON string for custom_properties
|
||||
val = data.get("custom_properties")
|
||||
if isinstance(val, str):
|
||||
import json
|
||||
try:
|
||||
data["custom_properties"] = json.loads(val)
|
||||
except Exception:
|
||||
pass
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
class RecordingSerializer(serializers.ModelSerializer):
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import os
|
|||
from rest_framework.decorators import action
|
||||
from django.conf import settings
|
||||
from .tasks import refresh_m3u_groups
|
||||
import json
|
||||
|
||||
from .models import M3UAccount, M3UFilter, ServerGroup, M3UAccountProfile
|
||||
from core.models import UserAgent
|
||||
|
|
@ -154,6 +155,7 @@ class M3UAccountViewSet(viewsets.ModelViewSet):
|
|||
enabled = setting.get("enabled", True)
|
||||
auto_sync = setting.get("auto_channel_sync", False)
|
||||
sync_start = setting.get("auto_sync_channel_start")
|
||||
custom_properties = setting.get("custom_properties", {})
|
||||
|
||||
if group_id:
|
||||
ChannelGroupM3UAccount.objects.update_or_create(
|
||||
|
|
@ -163,6 +165,11 @@ class M3UAccountViewSet(viewsets.ModelViewSet):
|
|||
"enabled": enabled,
|
||||
"auto_channel_sync": auto_sync,
|
||||
"auto_sync_channel_start": sync_start,
|
||||
"custom_properties": (
|
||||
custom_properties
|
||||
if isinstance(custom_properties, str)
|
||||
else json.dumps(custom_properties)
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -847,7 +847,6 @@ def sync_auto_channels(account_id):
|
|||
"""
|
||||
from apps.channels.models import Channel, ChannelGroup, ChannelGroupM3UAccount, Stream, ChannelStream
|
||||
from apps.epg.models import EPGData
|
||||
import json
|
||||
|
||||
try:
|
||||
account = M3UAccount.objects.get(id=account_id)
|
||||
|
|
@ -868,6 +867,28 @@ def sync_auto_channels(account_id):
|
|||
channel_group = group_relation.channel_group
|
||||
start_number = group_relation.auto_sync_channel_start or 1.0
|
||||
|
||||
# Get force_dummy_epg and group_override from group custom_properties
|
||||
group_custom_props = {}
|
||||
force_dummy_epg = False
|
||||
override_group_id = None
|
||||
if group_relation.custom_properties:
|
||||
try:
|
||||
group_custom_props = json.loads(group_relation.custom_properties)
|
||||
force_dummy_epg = group_custom_props.get("force_dummy_epg", False)
|
||||
override_group_id = group_custom_props.get("group_override")
|
||||
except Exception:
|
||||
force_dummy_epg = False
|
||||
override_group_id = None
|
||||
|
||||
# Determine which group to use for created channels
|
||||
target_group = channel_group
|
||||
if override_group_id:
|
||||
try:
|
||||
target_group = ChannelGroup.objects.get(id=override_group_id)
|
||||
logger.info(f"Using override group '{target_group.name}' instead of '{channel_group.name}' for auto-created channels")
|
||||
except ChannelGroup.DoesNotExist:
|
||||
logger.warning(f"Override group with ID {override_group_id} not found, using original group '{channel_group.name}'")
|
||||
|
||||
logger.info(f"Processing auto sync for group: {channel_group.name} (start: {start_number})")
|
||||
|
||||
# Get all current streams in this group for this M3U account
|
||||
|
|
@ -876,20 +897,22 @@ def sync_auto_channels(account_id):
|
|||
channel_group=channel_group
|
||||
).order_by('name')
|
||||
|
||||
# Get existing auto-created channels for this account in this group
|
||||
# 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
|
||||
existing_channels = Channel.objects.filter(
|
||||
channel_group=channel_group,
|
||||
auto_created=True,
|
||||
auto_created_by=account
|
||||
).select_related('logo', 'epg_data')
|
||||
|
||||
# Create mapping of existing channels by their associated stream
|
||||
# This approach finds channels even if they've been moved to different groups
|
||||
existing_channel_map = {}
|
||||
for channel in existing_channels:
|
||||
# Get streams associated with this channel that belong to our M3U account
|
||||
# Get streams associated with this channel that belong to our M3U account and original group
|
||||
channel_streams = ChannelStream.objects.filter(
|
||||
channel=channel,
|
||||
stream__m3u_account=account
|
||||
stream__m3u_account=account,
|
||||
stream__channel_group=channel_group # Match streams from the original group
|
||||
).select_related('stream')
|
||||
|
||||
# Map each of our M3U account's streams to this channel
|
||||
|
|
@ -903,9 +926,10 @@ def sync_auto_channels(account_id):
|
|||
if not current_streams.exists():
|
||||
logger.debug(f"No streams found in group {channel_group.name}")
|
||||
# Delete all existing auto channels if no streams
|
||||
if existing_channels.exists():
|
||||
deleted_count = existing_channels.count()
|
||||
existing_channels.delete()
|
||||
channels_to_delete = [ch for ch in existing_channel_map.values()]
|
||||
if channels_to_delete:
|
||||
deleted_count = len(channels_to_delete)
|
||||
Channel.objects.filter(id__in=[ch.id for ch in channels_to_delete]).delete()
|
||||
channels_deleted += deleted_count
|
||||
logger.debug(f"Deleted {deleted_count} auto channels (no streams remaining)")
|
||||
continue
|
||||
|
|
@ -941,6 +965,12 @@ def sync_auto_channels(account_id):
|
|||
existing_channel.tvc_guide_stationid = tvc_guide_stationid
|
||||
channel_updated = True
|
||||
|
||||
# Check if channel group needs to be updated (in case override was added/changed)
|
||||
if existing_channel.channel_group != target_group:
|
||||
existing_channel.channel_group = target_group
|
||||
channel_updated = True
|
||||
logger.info(f"Moved auto channel '{existing_channel.name}' from '{existing_channel.channel_group.name if existing_channel.channel_group else 'None'}' to '{target_group.name}'")
|
||||
|
||||
# Handle logo updates
|
||||
current_logo = None
|
||||
if stream.logo_url:
|
||||
|
|
@ -956,7 +986,7 @@ def sync_auto_channels(account_id):
|
|||
|
||||
# Handle EPG data updates
|
||||
current_epg_data = None
|
||||
if stream.tvg_id:
|
||||
if stream.tvg_id and not force_dummy_epg:
|
||||
current_epg_data = EPGData.objects.filter(tvg_id=stream.tvg_id).first()
|
||||
|
||||
if existing_channel.epg_data != current_epg_data:
|
||||
|
|
@ -974,13 +1004,13 @@ def sync_auto_channels(account_id):
|
|||
while Channel.objects.filter(channel_number=current_channel_number).exists():
|
||||
current_channel_number += 0.1
|
||||
|
||||
# Create the channel with auto-created tracking
|
||||
# Create the channel with auto-created tracking in the target group
|
||||
channel = Channel.objects.create(
|
||||
channel_number=current_channel_number,
|
||||
name=stream.name,
|
||||
tvg_id=stream.tvg_id,
|
||||
tvc_guide_stationid=tvc_guide_stationid,
|
||||
channel_group=channel_group,
|
||||
channel_group=target_group, # Use target group (could be override)
|
||||
user_level=0, # Default user level
|
||||
auto_created=True, # Mark as auto-created
|
||||
auto_created_by=account # Track which M3U account created it
|
||||
|
|
@ -994,11 +1024,14 @@ def sync_auto_channels(account_id):
|
|||
)
|
||||
|
||||
# Try to match EPG data
|
||||
if stream.tvg_id:
|
||||
if stream.tvg_id and not force_dummy_epg:
|
||||
epg_data = EPGData.objects.filter(tvg_id=stream.tvg_id).first()
|
||||
if epg_data:
|
||||
channel.epg_data = epg_data
|
||||
channel.save(update_fields=['epg_data'])
|
||||
elif stream.tvg_id and force_dummy_epg:
|
||||
channel.epg_data = None
|
||||
channel.save(update_fields=['epg_data'])
|
||||
|
||||
# Handle logo
|
||||
if stream.logo_url:
|
||||
|
|
|
|||
|
|
@ -739,7 +739,9 @@ export default class API {
|
|||
method: 'PATCH',
|
||||
body: { group_settings: groupSettings },
|
||||
});
|
||||
|
||||
// Fetch the updated playlist and update the store
|
||||
const updatedPlaylist = await API.getPlaylist(playlistId);
|
||||
usePlaylistsStore.getState().updatePlaylist(updatedPlaylist);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to update M3U group settings', e);
|
||||
|
|
@ -781,7 +783,6 @@ export default class API {
|
|||
const response = await request(`${host}/api/m3u/refresh/${id}/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to refresh M3U account', e);
|
||||
|
|
|
|||
|
|
@ -43,12 +43,26 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
|
|||
}
|
||||
|
||||
setGroupStates(
|
||||
playlist.channel_groups.map((group) => ({
|
||||
...group,
|
||||
name: channelGroups[group.channel_group].name,
|
||||
auto_channel_sync: group.auto_channel_sync || false,
|
||||
auto_sync_channel_start: group.auto_sync_channel_start || 1.0,
|
||||
}))
|
||||
playlist.channel_groups.map((group) => {
|
||||
// Parse custom_properties if present
|
||||
let customProps = {};
|
||||
if (group.custom_properties) {
|
||||
try {
|
||||
customProps = typeof group.custom_properties === 'string'
|
||||
? JSON.parse(group.custom_properties)
|
||||
: group.custom_properties;
|
||||
} catch (e) {
|
||||
customProps = {};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...group,
|
||||
name: channelGroups[group.channel_group].name,
|
||||
auto_channel_sync: group.auto_channel_sync || false,
|
||||
auto_sync_channel_start: group.auto_sync_channel_start || 1.0,
|
||||
custom_properties: customProps,
|
||||
};
|
||||
})
|
||||
);
|
||||
}, [playlist, channelGroups]);
|
||||
|
||||
|
|
@ -79,11 +93,36 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// Toggle force_dummy_epg in custom_properties for a group
|
||||
const toggleForceDummyEPG = (id) => {
|
||||
setGroupStates(
|
||||
groupStates.map((state) => {
|
||||
if (state.channel_group == id) {
|
||||
const customProps = { ...(state.custom_properties || {}) };
|
||||
customProps.force_dummy_epg = !customProps.force_dummy_epg;
|
||||
return {
|
||||
...state,
|
||||
custom_properties: customProps,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Prepare groupStates for API: custom_properties must be stringified
|
||||
const payload = groupStates.map((state) => ({
|
||||
...state,
|
||||
custom_properties: state.custom_properties
|
||||
? JSON.stringify(state.custom_properties)
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
// Update group settings via API endpoint
|
||||
await API.updateM3UGroupSettings(playlist.id, groupStates);
|
||||
await API.updateM3UGroupSettings(playlist.id, payload);
|
||||
|
||||
// Show notification about the refresh process
|
||||
notifications.show({
|
||||
|
|
@ -215,24 +254,95 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
|
|||
|
||||
{/* Auto Sync Controls */}
|
||||
<Stack spacing={4}>
|
||||
<Checkbox
|
||||
label="Auto Channel Sync"
|
||||
checked={group.auto_channel_sync && group.enabled}
|
||||
disabled={!group.enabled}
|
||||
onChange={() => toggleAutoSync(group.channel_group)}
|
||||
size="xs"
|
||||
/>
|
||||
<Flex align="center" gap="xs">
|
||||
<Checkbox
|
||||
label="Auto Channel Sync"
|
||||
checked={group.auto_channel_sync && group.enabled}
|
||||
disabled={!group.enabled}
|
||||
onChange={() => toggleAutoSync(group.channel_group)}
|
||||
size="xs"
|
||||
/>
|
||||
{group.auto_channel_sync && group.enabled && (
|
||||
<Checkbox
|
||||
label="Force Dummy EPG"
|
||||
checked={!!(group.custom_properties && group.custom_properties.force_dummy_epg)}
|
||||
onChange={() => toggleForceDummyEPG(group.channel_group)}
|
||||
size="xs"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{group.auto_channel_sync && group.enabled && (
|
||||
<NumberInput
|
||||
label="Start Channel #"
|
||||
value={group.auto_sync_channel_start}
|
||||
onChange={(value) => updateChannelStart(group.channel_group, value)}
|
||||
min={1}
|
||||
step={1}
|
||||
size="xs"
|
||||
precision={1}
|
||||
/>
|
||||
<>
|
||||
<NumberInput
|
||||
label="Start Channel #"
|
||||
value={group.auto_sync_channel_start}
|
||||
onChange={(value) => updateChannelStart(group.channel_group, value)}
|
||||
min={1}
|
||||
step={1}
|
||||
size="xs"
|
||||
precision={1}
|
||||
/>
|
||||
|
||||
{/* Override Channel Group */}
|
||||
<Flex align="center" gap="xs">
|
||||
<Checkbox
|
||||
checked={!!(group.custom_properties && Object.prototype.hasOwnProperty.call(group.custom_properties, 'group_override'))}
|
||||
onChange={(event) => {
|
||||
const isEnabled = event.currentTarget.checked;
|
||||
setGroupStates(
|
||||
groupStates.map((state) => {
|
||||
if (state.channel_group == group.channel_group) {
|
||||
const newCustomProps = { ...(state.custom_properties || {}) };
|
||||
if (isEnabled) {
|
||||
newCustomProps.group_override = null;
|
||||
} else {
|
||||
delete newCustomProps.group_override;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
custom_properties: newCustomProps,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
})
|
||||
);
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
<Select
|
||||
label="Override Channel Group"
|
||||
placeholder="Choose group..."
|
||||
value={group.custom_properties?.group_override?.toString() || null}
|
||||
onChange={(value) => {
|
||||
const newValue = value ? parseInt(value) : null;
|
||||
setGroupStates(
|
||||
groupStates.map((state) => {
|
||||
if (state.channel_group == group.channel_group) {
|
||||
return {
|
||||
...state,
|
||||
custom_properties: {
|
||||
...state.custom_properties,
|
||||
group_override: newValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
return state;
|
||||
})
|
||||
);
|
||||
}}
|
||||
data={Object.values(channelGroups).map((g) => ({
|
||||
value: g.id.toString(),
|
||||
label: g.name,
|
||||
}))}
|
||||
disabled={!(group.custom_properties && Object.prototype.hasOwnProperty.call(group.custom_properties, 'group_override'))}
|
||||
clearable
|
||||
searchable
|
||||
size="xs"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
|
|
@ -259,4 +369,4 @@ const M3UGroupFilter = ({ playlist = null, isOpen, onClose }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default M3UGroupFilter;
|
||||
export default M3UGroupFilter;
|
||||
|
|
@ -288,7 +288,8 @@ const ChannelsTable = ({ }) => {
|
|||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [hdhrUrl, setHDHRUrl] = useState(hdhrUrlBase);
|
||||
const [epgUrl, setEPGUrl] = useState(epgUrlBase); const [m3uUrl, setM3UUrl] = useState(m3uUrlBase);
|
||||
const [epgUrl, setEPGUrl] = useState(epgUrlBase);
|
||||
const [m3uUrl, setM3UUrl] = useState(m3uUrlBase);
|
||||
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
|
|
@ -308,7 +309,7 @@ const ChannelsTable = ({ }) => {
|
|||
});
|
||||
|
||||
/**
|
||||
* Dereived variables
|
||||
* Derived variables
|
||||
*/
|
||||
const activeGroupIds = new Set(
|
||||
Object.values(channels).map((channel) => channel.channel_group_id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue