From 64b1ca80296c880f8202adefb30d0948d5598026 Mon Sep 17 00:00:00 2001 From: Roland Date: Fri, 3 Jul 2026 17:29:24 -0600 Subject: [PATCH] Channels: fix statistics inflated by tagged videos The tag_file join in Channel.get_statistics() produced one row per tag, so a Video with N Tags was counted N times in video_count, size, and length. Count tagged Videos in a subquery instead. Co-Authored-By: Claude Fable 5 --- modules/videos/channel/test/test_models.py | 34 ++++++++++++++++++++++ modules/videos/models.py | 16 +++++----- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/modules/videos/channel/test/test_models.py b/modules/videos/channel/test/test_models.py index 7f2dbdfa..e3a21afd 100644 --- a/modules/videos/channel/test/test_models.py +++ b/modules/videos/channel/test/test_models.py @@ -110,6 +110,40 @@ async def test_channel_download_relationships(test_session, download_channel): assert test_session.query(Download).count() == 0 +@pytest.mark.asyncio +async def test_channel_statistics_with_tagged_videos(async_client, test_session, channel_factory, video_factory, + tag_factory): + """Tagging a Video must not inflate its Channel's statistics. + + A Video with multiple Tags was counted once per Tag because of the tag_file join.""" + channel = channel_factory() + video1 = video_factory(channel_id=channel.id) + video2 = video_factory(channel_id=channel.id) + video3 = video_factory(channel_id=channel.id) + test_session.commit() + + expected_size = video1.file_group.size + video2.file_group.size + video3.file_group.size + + # Statistics are correct before any Videos are tagged. + statistics = channel.get_statistics() + assert statistics['video_count'] == 3 + assert statistics['size'] == expected_size + assert statistics['video_tags'] == 0 + + # video1 has two Tags, video2 has one Tag, video3 has none. + tag1, tag2 = await tag_factory(), await tag_factory() + video1.add_tag(test_session, tag1.name) + video1.add_tag(test_session, tag2.name) + video2.add_tag(test_session, tag1.name) + test_session.commit() + + # Tagging Videos must not change the number of Videos in the Channel. + statistics = channel.get_statistics() + assert statistics['video_count'] == 3 + assert statistics['size'] == expected_size + assert statistics['video_tags'] == 2 + + def test_channel_info_json(test_session, test_directory): from wrolpi.collections import Collection diff --git a/modules/videos/models.py b/modules/videos/models.py index 83064a9b..02379696 100644 --- a/modules/videos/models.py +++ b/modules/videos/models.py @@ -953,15 +953,17 @@ class Channel(ModelHelper, Base): """Get statistics about this channel.""" with get_db_curs() as curs: stmt = ''' - SELECT SUM(size) AS "size", - MAX(size) AS "largest_video", - COUNT(video.id) AS "video_count", - SUM(fg.length) AS "length", - -- Videos may use multiple tags. - COUNT(video.id) FILTER ( WHERE tf.file_group_id IS NOT NULL ) AS "video_tags" + SELECT SUM(size) AS "size", + MAX(size) AS "largest_video", + COUNT(video.id) AS "video_count", + SUM(fg.length) AS "length", + -- Count Videos with at least one Tag; a Video may use multiple Tags. + (SELECT COUNT(DISTINCT tf.file_group_id) + FROM tag_file tf + INNER JOIN video v ON v.file_group_id = tf.file_group_id + WHERE v.channel_id = %(id)s) AS "video_tags" FROM video LEFT JOIN file_group fg on fg.id = video.file_group_id - LEFT JOIN public.tag_file tf on fg.id = tf.file_group_id WHERE channel_id = %(id)s \ ''' curs.execute(stmt, dict(id=self.id))