From 0aed30253b986f2e2c45fec754ab0f4ffd0f79b0 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 17 Jul 2026 23:21:44 +0000 Subject: [PATCH] refactor(stats): simplify channel logo handling in stats data This update refactors the logic for building timeshift statistics by removing unnecessary `select_related` calls and the `logo_url` field from the connections. The changes streamline the data retrieval process and ensure that only the `logo_id` is exposed in the session data. Corresponding tests have been updated to reflect these modifications. --- apps/timeshift/stats.py | 7 +- apps/timeshift/tests/test_stats.py | 18 +++- .../cards/TimeshiftConnectionCard.jsx | 5 +- .../TimeshiftConnectionCard.test.jsx | 85 +++++++++++++++++++ 4 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/cards/__tests__/TimeshiftConnectionCard.test.jsx diff --git a/apps/timeshift/stats.py b/apps/timeshift/stats.py index ff4e3066..ebc54249 100644 --- a/apps/timeshift/stats.py +++ b/apps/timeshift/stats.py @@ -490,9 +490,7 @@ def build_timeshift_stats_data(redis_client): channel_ids = {conn["channel_id"] for conn in connections if conn.get("channel_id")} channels_by_id = {} if channel_ids: - for channel in Channel.objects.select_related("logo", "epg_data").filter( - id__in=channel_ids, - ): + for channel in Channel.objects.filter(id__in=channel_ids): channels_by_id[channel.id] = channel profile_ids = { @@ -516,8 +514,6 @@ def build_timeshift_stats_data(redis_client): conn["channel_name"] = channel.name if channel.logo_id: conn["logo_id"] = channel.logo_id - if channel.logo: - conn["logo_url"] = channel.logo.url profile = profiles_by_id.get(conn.get("m3u_profile_id")) if profile: @@ -546,7 +542,6 @@ def build_timeshift_stats_data(redis_client): "channel_uuid": conn.get("channel_uuid", ""), "channel_name": conn["channel_name"], "logo_id": conn.get("logo_id"), - "logo_url": conn.get("logo_url"), "programme_start": conn["programme_start"], "position_anchor_at": position_anchor_at, "playback_base_secs": playback_base_secs, diff --git a/apps/timeshift/tests/test_stats.py b/apps/timeshift/tests/test_stats.py index 0503a4ea..db2b30b6 100644 --- a/apps/timeshift/tests/test_stats.py +++ b/apps/timeshift/tests/test_stats.py @@ -300,7 +300,7 @@ class BuildTimeshiftStatsDataTests(TestCase): channel.uuid = "00000000-0000-0000-0000-000000000042" channel.logo_id = None channel.logo = None - mock_channel_model.objects.select_related.return_value.filter.return_value = [ + mock_channel_model.objects.filter.return_value = [ channel, ] payload = build_timeshift_stats_data(self.redis) @@ -317,6 +317,18 @@ class BuildTimeshiftStatsDataTests(TestCase): self.assertFalse(session["paused"]) self.assertEqual(session["connections"][0]["ip_address"], "10.0.0.5") + @patch("apps.timeshift.stats.Channel") + def test_build_timeshift_stats_exposes_logo_id_only(self, mock_channel_model): + channel = MagicMock() + channel.id = self.channel_id + channel.name = "Catch-up Stats Channel" + channel.uuid = "00000000-0000-0000-0000-000000000042" + channel.logo_id = 77 + mock_channel_model.objects.filter.return_value = [channel] + session = build_timeshift_stats_data(self.redis)["timeshift_sessions"][0] + self.assertEqual(session["logo_id"], 77) + self.assertNotIn("logo_url", session) + def test_find_stats_channel_for_session(self): found = find_stats_channel_for_session(self.redis, self.session_id) self.assertEqual(found, self.stats_channel_id) @@ -344,7 +356,7 @@ class BuildTimeshiftStatsDataTests(TestCase): channel.uuid = "00000000-0000-0000-0000-000000000042" channel.logo_id = None channel.logo = None - mock_channel_model.objects.select_related.return_value.filter.return_value = [ + mock_channel_model.objects.filter.return_value = [ channel, ] client_key = RedisKeys.client_metadata(self.stats_channel_id, self.session_id) @@ -355,7 +367,7 @@ class BuildTimeshiftStatsDataTests(TestCase): @patch("apps.timeshift.stats.Channel") def test_skips_clients_missing_required_metadata(self, mock_channel_model): - mock_channel_model.objects.select_related.return_value.filter.return_value = [] + mock_channel_model.objects.filter.return_value = [] orphan_key = RedisKeys.client_metadata(self.stats_channel_id, "orphan") self.redis.hset(orphan_key, mapping={"user_agent": "VLC"}) self.redis.sadd(RedisKeys.clients(self.stats_channel_id), "orphan") diff --git a/frontend/src/components/cards/TimeshiftConnectionCard.jsx b/frontend/src/components/cards/TimeshiftConnectionCard.jsx index fedec0ed..f95dfb99 100644 --- a/frontend/src/components/cards/TimeshiftConnectionCard.jsx +++ b/frontend/src/components/cards/TimeshiftConnectionCard.jsx @@ -135,10 +135,7 @@ const TimeshiftConnectionCard = ({ timeshiftSession.individual_connection || (timeshiftSession.connections && timeshiftSession.connections[0]); - const logoUrl = - timeshiftSession.logo_url || - getLogoUrl(timeshiftSession.logo_id, logos) || - logo; + const logoUrl = getLogoUrl(timeshiftSession.logo_id, logos) || logo; const programmePreview = useMemo(() => { if (currentProgram?.title) { diff --git a/frontend/src/components/cards/__tests__/TimeshiftConnectionCard.test.jsx b/frontend/src/components/cards/__tests__/TimeshiftConnectionCard.test.jsx new file mode 100644 index 00000000..b26e7bb0 --- /dev/null +++ b/frontend/src/components/cards/__tests__/TimeshiftConnectionCard.test.jsx @@ -0,0 +1,85 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('../../../utils/dateTimeUtils.js', () => ({ + useDateTimeFormat: vi.fn(() => ({ fullDateTimeFormat: 'MM/DD/YYYY h:mm A' })), +})); + +vi.mock('../../../store/users.jsx', () => ({ + default: vi.fn(() => []), +})); + +vi.mock('../../../images/logo.png', () => ({ default: 'default-logo.png' })); + +vi.mock('../../../utils/cards/StreamConnectionCardUtils.js', () => ({ + getLogoUrl: vi.fn(), +})); + +vi.mock('../../../utils/cards/TimeshiftConnectionCardUtils.js', () => ({ + calculateConnectionDuration: vi.fn(() => '5m'), + calculateConnectionStartTime: vi.fn(() => 'Jan 1 2024 10:00 AM'), + computeCatchupPlaybackSeconds: vi.fn(() => 0), + getConnectionDurationSeconds: vi.fn(() => 0), +})); + +vi.mock('@mantine/core', () => ({ + ActionIcon: ({ children }) => , + Badge: ({ children }) => {children}, + Box: ({ children }) =>
{children}
, + Card: ({ children }) =>
{children}
, + Center: ({ children }) =>
{children}
, + Group: ({ children }) =>
{children}
, + Stack: ({ children }) =>
{children}
, + Text: ({ children }) => {children}, + Tooltip: ({ children }) => <>{children}, +})); + +vi.mock('lucide-react', () => ({ + ChevronDown: () => null, + HardDriveUpload: () => null, + History: () => null, + SquareX: () => null, + Timer: () => null, +})); + +vi.mock('../../ProgramPreview.jsx', () => ({ + default: () =>
, +})); + +import TimeshiftConnectionCard from '../TimeshiftConnectionCard.jsx'; +import { getLogoUrl } from '../../../utils/cards/StreamConnectionCardUtils.js'; + +describe('TimeshiftConnectionCard logos', () => { + beforeEach(() => { + vi.clearAllMocks(); + getLogoUrl.mockReturnValue('/api/channels/logos/77/cache/'); + }); + + it('resolves the channel logo via logo_id and the logos store', () => { + render( + + ); + + expect(getLogoUrl).toHaveBeenCalledWith(77, { + 77: { cache_url: '/api/channels/logos/77/cache/' }, + }); + const img = screen.getByAltText('channel logo'); + expect(img.getAttribute('src')).toBe('/api/channels/logos/77/cache/'); + }); +});