From 95154c56e77ec8e99df1465985e223fabc95dc61 Mon Sep 17 00:00:00 2001 From: AB Date: Sun, 20 Sep 2026 20:03:55 +0300 Subject: [PATCH] Fixed video statistics --- src/analytics.rs | 21 +++++++++++++++++++++ src/public.rs | 19 ++++++++++++++----- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/analytics.rs b/src/analytics.rs index d4cfd14..0711455 100644 --- a/src/analytics.rs +++ b/src/analytics.rs @@ -315,6 +315,27 @@ pub async fn load_events( ) } +/// Whether this visitor already opened this media within the given window. +/// +/// Used to count a media open once regardless of storage backend or file type: +/// a video streams via many HTTP range requests, and this collapses them (plus +/// quick re-opens) into a single counted view. +pub async fn media_viewed_recently( + db: &Database, + media_id: i64, + visitor: &str, + within: Duration, +) -> cot::Result { + let cutoff = chrono::Utc::now().naive_utc() - within; + let visitor_owned = visitor.to_string(); + let events = query!(AnalyticsEvent, $visitor_hash == visitor_owned) + .all(db) + .await?; + Ok(events.iter().any(|e| { + e.event_type == "media_view" && e.media_id == Some(media_id) && e.created_at >= cutoff + })) +} + /// Loads a page of recent events, newest first, for the event log. /// /// Cursor pagination by descending id: pass the id of the oldest event already diff --git a/src/public.rs b/src/public.rs index 6f9816c..e8c978d 100644 --- a/src/public.rs +++ b/src/public.rs @@ -940,11 +940,20 @@ async fn portal_media( .and_then(|value| value.to_str().ok()) .map(str::to_owned); - // Analytics: count full-size media opens. Only count the initial request, - // not subsequent HTTP range/seek requests for the same file. - if range.is_none() { - let visitor_id = crate::analytics::visitor_id_from_request(&request) - .unwrap_or_else(crate::analytics::new_visitor_id); + // Analytics: count full-size media opens (photos and videos alike). A video + // streams via many HTTP range requests, so we dedupe by visitor+media within + // a short window to count a single open instead of every chunk/seek. + let visitor_id = crate::analytics::visitor_id_from_request(&request) + .unwrap_or_else(crate::analytics::new_visitor_id); + let recently_viewed = crate::analytics::media_viewed_recently( + &db, + media_id, + &visitor_id, + chrono::Duration::minutes(30), + ) + .await + .unwrap_or(false); + if !recently_viewed { crate::analytics::record( &db, EventType::MediaView,