mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
Merge branch 'dev' of https://github.com/Dispatcharr/Dispatcharr into dev
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run
This commit is contained in:
commit
ab8fd29434
4 changed files with 161 additions and 3 deletions
|
|
@ -644,6 +644,34 @@ class NumbersInRangeLookupTests(TestCase):
|
|||
)
|
||||
self.assertFalse(occupant["has_channel_number_override"])
|
||||
|
||||
def test_group_override_channel_reports_target_group(self):
|
||||
# When auto-sync routes channels into a different group via
|
||||
# group_override, the occupant's channel_group_id is the override
|
||||
# target, not the source group being configured. The frontend relies
|
||||
# on this to recognize override-routed channels as the config's own
|
||||
# output (effectiveSyncGroupId), so the warning does not flag them.
|
||||
account = _make_account()
|
||||
source = _make_group(name="SourceGrp")
|
||||
target = _make_group(name="TargetGrp")
|
||||
Channel.objects.create(
|
||||
name="Routed",
|
||||
channel_number=3210,
|
||||
channel_group=target,
|
||||
auto_created=True,
|
||||
auto_created_by=account,
|
||||
)
|
||||
client = self._client()
|
||||
|
||||
response = client.get(
|
||||
"/api/channels/channels/numbers-in-range/?start=3210&end=3210"
|
||||
)
|
||||
|
||||
occupant = response.data["occupants"][0]
|
||||
self.assertEqual(occupant["channel_group_id"], target.id)
|
||||
self.assertNotEqual(occupant["channel_group_id"], source.id)
|
||||
self.assertTrue(occupant["auto_created"])
|
||||
self.assertEqual(occupant["auto_created_by_account_id"], account.id)
|
||||
|
||||
def test_manual_channel_exposed_with_auto_created_false(self):
|
||||
# Manual channels are always a real collision worth surfacing.
|
||||
# The response must flag them with auto_created=False and a null
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
getRegexOptions,
|
||||
getStreamsRegexPreview,
|
||||
isExpectedOccupantForGroup,
|
||||
effectiveSyncGroupId,
|
||||
isGroupVisible,
|
||||
rangeFor,
|
||||
} from '../../utils/forms/LiveGroupFilterUtils.js';
|
||||
|
|
@ -131,7 +132,12 @@ const LiveGroupFilter = ({
|
|||
// (in-memory range overlap with sibling groups) sources so the sweep
|
||||
// can refresh form-overlap synchronously without firing HTTP for
|
||||
// groups that did not change.
|
||||
const scheduleConflictScan = (groupId, rawStart, rawEnd) => {
|
||||
const scheduleConflictScan = (
|
||||
groupId,
|
||||
rawStart,
|
||||
rawEnd,
|
||||
expectedGroupId = groupId
|
||||
) => {
|
||||
if (conflictTimersRef.current[groupId]) {
|
||||
clearTimeout(conflictTimersRef.current[groupId]);
|
||||
}
|
||||
|
|
@ -156,7 +162,7 @@ const LiveGroupFilter = ({
|
|||
? result.occupants
|
||||
: [];
|
||||
const unexpected = occupants.filter(
|
||||
(o) => !isExpectedOccupantForGroup(o, groupId, playlist)
|
||||
(o) => !isExpectedOccupantForGroup(o, expectedGroupId, playlist)
|
||||
);
|
||||
setConflictSource(groupId, 'occupant', unexpected.length > 0);
|
||||
} catch (e) {
|
||||
|
|
@ -221,7 +227,8 @@ const LiveGroupFilter = ({
|
|||
scheduleConflictScan(
|
||||
g.channel_group,
|
||||
range.startRaw,
|
||||
g.auto_sync_channel_end
|
||||
g.auto_sync_channel_end,
|
||||
effectiveSyncGroupId(g)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,18 @@ export const isExpectedOccupantForGroup = (
|
|||
);
|
||||
};
|
||||
|
||||
// The group the sync's own channels actually land in. A group_override
|
||||
// routes auto-created channels into a different ChannelGroup, so the
|
||||
// conflict check must recognize occupants of that target group as this
|
||||
// config's own output rather than flagging them against the source group.
|
||||
export const effectiveSyncGroupId = (group) => {
|
||||
const override = group?.custom_properties?.group_override;
|
||||
if (override !== undefined && override !== null && override !== '') {
|
||||
return Number(override);
|
||||
}
|
||||
return group?.channel_group;
|
||||
};
|
||||
|
||||
export const rangeFor = (g) => {
|
||||
if (!g.enabled || !g.auto_channel_sync) return null;
|
||||
const mode = g.custom_properties?.channel_numbering_mode || 'fixed';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
getChannelsInRange,
|
||||
getStreamsRegexPreview,
|
||||
isExpectedOccupantForGroup,
|
||||
effectiveSyncGroupId,
|
||||
rangeFor,
|
||||
abortTimers,
|
||||
getRegexOptions,
|
||||
|
|
@ -227,6 +228,116 @@ describe('LiveGroupFilterUtils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── effectiveSyncGroupId ───────────────────────────────────────────────────
|
||||
describe('effectiveSyncGroupId', () => {
|
||||
it('returns the source channel_group when there is no override', () => {
|
||||
expect(effectiveSyncGroupId(makeGroup({ channel_group: 7 }))).toBe(7);
|
||||
});
|
||||
|
||||
it('returns the group_override target when set', () => {
|
||||
const group = makeGroup({
|
||||
channel_group: 7,
|
||||
custom_properties: { group_override: 9 },
|
||||
});
|
||||
expect(effectiveSyncGroupId(group)).toBe(9);
|
||||
});
|
||||
|
||||
it('coerces a string-stored group_override to a number', () => {
|
||||
const group = makeGroup({
|
||||
channel_group: 7,
|
||||
custom_properties: { group_override: '9' },
|
||||
});
|
||||
expect(effectiveSyncGroupId(group)).toBe(9);
|
||||
});
|
||||
|
||||
it('falls back to the source group when group_override is blank', () => {
|
||||
const group = makeGroup({
|
||||
channel_group: 7,
|
||||
custom_properties: { group_override: '' },
|
||||
});
|
||||
expect(effectiveSyncGroupId(group)).toBe(7);
|
||||
});
|
||||
|
||||
// Regression guard for the group-override range-conflict false positive:
|
||||
// the auto-sync's own channels land in the override target group, so
|
||||
// comparing against the source group (pre-fix) flags them as a conflict,
|
||||
// while comparing against the effective target recognizes them as this
|
||||
// config's own output.
|
||||
it("makes group-override occupants count as this group's own", () => {
|
||||
const group = makeGroup({
|
||||
channel_group: 7,
|
||||
custom_properties: { group_override: 9 },
|
||||
});
|
||||
const occupant = makeOccupant({ channel_group_id: 9 });
|
||||
// Pre-fix comparison (source group) treats own channels as a conflict.
|
||||
expect(
|
||||
isExpectedOccupantForGroup(
|
||||
occupant,
|
||||
group.channel_group,
|
||||
makePlaylist()
|
||||
)
|
||||
).toBe(false);
|
||||
// Comparing against the effective target recognizes them as expected.
|
||||
expect(
|
||||
isExpectedOccupantForGroup(
|
||||
occupant,
|
||||
effectiveSyncGroupId(group),
|
||||
makePlaylist()
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Guards against over-suppression: resolving the effective target group
|
||||
// must still surface genuine collisions in an override config's range.
|
||||
// Only the config's own output (auto-created, this account, in the
|
||||
// target group, unpinned) is excluded.
|
||||
it('still flags genuine collisions in a group-override config', () => {
|
||||
const group = makeGroup({
|
||||
channel_group: 7,
|
||||
custom_properties: { group_override: 9 },
|
||||
});
|
||||
const target = effectiveSyncGroupId(group);
|
||||
// Manual channel sitting in the range.
|
||||
expect(
|
||||
isExpectedOccupantForGroup(
|
||||
makeOccupant({ channel_group_id: 9, auto_created: false }),
|
||||
target,
|
||||
makePlaylist()
|
||||
)
|
||||
).toBe(false);
|
||||
// Auto-created by a different account.
|
||||
expect(
|
||||
isExpectedOccupantForGroup(
|
||||
makeOccupant({
|
||||
channel_group_id: 9,
|
||||
auto_created_by_account_id: 999,
|
||||
}),
|
||||
target,
|
||||
makePlaylist()
|
||||
)
|
||||
).toBe(false);
|
||||
// A channel in a different group than the override target.
|
||||
expect(
|
||||
isExpectedOccupantForGroup(
|
||||
makeOccupant({ channel_group_id: 123 }),
|
||||
target,
|
||||
makePlaylist()
|
||||
)
|
||||
).toBe(false);
|
||||
// A user-pinned channel number.
|
||||
expect(
|
||||
isExpectedOccupantForGroup(
|
||||
makeOccupant({
|
||||
channel_group_id: 9,
|
||||
has_channel_number_override: true,
|
||||
}),
|
||||
target,
|
||||
makePlaylist()
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── rangeFor ──────────────────────────────────────────────────────────────
|
||||
describe('rangeFor', () => {
|
||||
it('returns null when group is disabled', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue