Skip to main content

zantetsu_core/scoring/
context.rs

1use serde::{Deserialize, Serialize};
2
3use crate::types::VideoCodec;
4
5use super::profile::QualityScores;
6
7/// Device type affects resolution preference.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub enum DeviceType {
10    /// Desktop computer — no penalty.
11    Desktop,
12    /// Laptop — slight preference for 1080p over 4K.
13    Laptop,
14    /// Mobile device — strong preference for 720p.
15    Mobile,
16    /// Television — preference for highest resolution.
17    TV,
18    /// Embedded device — SD/720p cap.
19    Embedded,
20}
21
22/// Network quality affects bitrate tolerance.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum NetworkQuality {
25    /// No bandwidth constraints.
26    Unlimited,
27    /// Broadband — slight penalty for 4K remux.
28    Broadband,
29    /// Limited connection — strong penalty for large files.
30    Limited,
31    /// Offline — only locally cached files.
32    Offline,
33}
34
35/// Client context for dynamic score adjustment.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ClientContext {
38    /// Device type affects resolution preference.
39    pub device_type: DeviceType,
40    /// Network condition affects bitrate tolerance.
41    pub network: NetworkQuality,
42    /// Hardware-supported video codecs on the client.
43    pub hw_decode_codecs: Vec<VideoCodec>,
44}
45
46impl Default for ClientContext {
47    fn default() -> Self {
48        Self {
49            device_type: DeviceType::Desktop,
50            network: NetworkQuality::Unlimited,
51            hw_decode_codecs: vec![VideoCodec::H264, VideoCodec::HEVC],
52        }
53    }
54}
55
56impl ClientContext {
57    /// Applies context-aware multipliers to the quality scores.
58    ///
59    /// Returns the adjusted final score.
60    #[must_use]
61    pub fn adjust_score(
62        &self,
63        mut scores: QualityScores,
64        file_video_codec: Option<VideoCodec>,
65    ) -> QualityScores {
66        // Device-type resolution adjustment
67        if let Some(ref mut res_score) = scores.resolution {
68            let multiplier = self.resolution_multiplier(*res_score);
69            *res_score *= multiplier;
70        }
71
72        // Network penalty (applied as a global modifier to all scores)
73        let network_mult = self.network_multiplier();
74        if let Some(ref mut res) = scores.resolution {
75            *res *= network_mult;
76        }
77        if let Some(ref mut vc) = scores.video_codec {
78            *vc *= network_mult;
79        }
80
81        // Hardware decoding penalty
82        if let Some(codec) = file_video_codec
83            && !self.hw_decode_codecs.contains(&codec)
84        {
85            // Massive penalty: codec not hardware-decodable
86            scores.video_codec = scores.video_codec.map(|s| s * 0.1);
87        }
88
89        scores
90    }
91
92    /// Returns a resolution multiplier based on device type.
93    fn resolution_multiplier(&self, res_score: f32) -> f32 {
94        match self.device_type {
95            DeviceType::Desktop | DeviceType::TV => 1.0,
96            DeviceType::Laptop => {
97                if res_score > 0.9 {
98                    // 4K gets slightly penalized on laptops
99                    0.85
100                } else {
101                    1.0
102                }
103            }
104            DeviceType::Mobile => {
105                if res_score > 0.6 {
106                    // Anything above 720p gets penalized on mobile
107                    0.6
108                } else {
109                    1.0
110                }
111            }
112            DeviceType::Embedded => {
113                if res_score > 0.5 {
114                    // Embedded caps at 720p effective preference
115                    0.5
116                } else {
117                    1.0
118                }
119            }
120        }
121    }
122
123    /// Returns a network quality multiplier.
124    fn network_multiplier(&self) -> f32 {
125        match self.network {
126            NetworkQuality::Unlimited => 1.0,
127            NetworkQuality::Broadband => 0.9,
128            NetworkQuality::Limited => 0.3,
129            NetworkQuality::Offline => 1.0, // No penalty; file is already local
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::types::{AudioCodec, MediaSource, Resolution};
138
139    fn make_scores(
140        resolution: Option<Resolution>,
141        video_codec: Option<VideoCodec>,
142    ) -> QualityScores {
143        QualityScores::from_metadata(
144            resolution,
145            video_codec,
146            Some(AudioCodec::AAC),
147            Some(MediaSource::WebDL),
148            0.7,
149        )
150    }
151
152    #[test]
153    fn desktop_unlimited_no_penalty() {
154        let ctx = ClientContext::default();
155        let scores = make_scores(Some(Resolution::UHD2160), Some(VideoCodec::H264));
156        let adjusted = ctx.adjust_score(scores.clone(), Some(VideoCodec::H264));
157
158        // Desktop + Unlimited + H264 (in hw_decode_codecs) → no penalty
159        assert_eq!(adjusted.resolution, scores.resolution);
160    }
161
162    #[test]
163    fn mobile_penalizes_high_resolution() {
164        let ctx = ClientContext {
165            device_type: DeviceType::Mobile,
166            network: NetworkQuality::Unlimited,
167            hw_decode_codecs: vec![VideoCodec::H264, VideoCodec::HEVC],
168        };
169
170        let scores = make_scores(Some(Resolution::FHD1080), Some(VideoCodec::H264));
171        let adjusted = ctx.adjust_score(scores, Some(VideoCodec::H264));
172
173        // 1080p score (0.85) is > 0.6 threshold → multiplied by 0.6
174        let expected = 0.85 * 0.6;
175        assert!(
176            (adjusted.resolution.unwrap() - expected).abs() < 0.001,
177            "got {}, expected {}",
178            adjusted.resolution.unwrap(),
179            expected
180        );
181    }
182
183    #[test]
184    fn limited_network_penalizes_all() {
185        let ctx = ClientContext {
186            device_type: DeviceType::Desktop,
187            network: NetworkQuality::Limited,
188            hw_decode_codecs: vec![VideoCodec::H264],
189        };
190
191        let scores = make_scores(Some(Resolution::FHD1080), Some(VideoCodec::H264));
192        let adjusted = ctx.adjust_score(scores, Some(VideoCodec::H264));
193
194        // Limited network → 0.3 multiplier on resolution and video codec
195        let expected_res = 0.85 * 0.3;
196        assert!((adjusted.resolution.unwrap() - expected_res).abs() < 0.001);
197    }
198
199    #[test]
200    fn unsupported_codec_massive_penalty() {
201        let ctx = ClientContext {
202            device_type: DeviceType::Desktop,
203            network: NetworkQuality::Unlimited,
204            hw_decode_codecs: vec![VideoCodec::H264], // AV1 NOT listed
205        };
206
207        let scores = make_scores(Some(Resolution::FHD1080), Some(VideoCodec::AV1));
208        let adjusted = ctx.adjust_score(scores, Some(VideoCodec::AV1));
209
210        // AV1 score (1.0) * 0.1 = 0.1
211        assert!((adjusted.video_codec.unwrap() - 0.1).abs() < 0.001);
212    }
213
214    #[test]
215    fn default_context_is_desktop_unlimited() {
216        let ctx = ClientContext::default();
217        assert_eq!(ctx.device_type, DeviceType::Desktop);
218        assert_eq!(ctx.network, NetworkQuality::Unlimited);
219        assert!(ctx.hw_decode_codecs.contains(&VideoCodec::H264));
220        assert!(ctx.hw_decode_codecs.contains(&VideoCodec::HEVC));
221    }
222}