zantetsu_core/scoring/
profile.rs1use serde::{Deserialize, Serialize};
2
3use crate::types::{AudioCodec, MediaSource, Resolution, VideoCodec};
4
5pub const WEIGHT_RESOLUTION: f32 = 0.35;
7pub const WEIGHT_VIDEO_CODEC: f32 = 0.25;
8pub const WEIGHT_AUDIO_CODEC: f32 = 0.15;
9pub const WEIGHT_SOURCE: f32 = 0.15;
10pub const WEIGHT_GROUP_TRUST: f32 = 0.10;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct QualityProfile {
15 pub resolution_weight: f32,
16 pub video_codec_weight: f32,
17 pub audio_codec_weight: f32,
18 pub source_weight: f32,
19 pub group_trust_weight: f32,
20}
21
22impl Default for QualityProfile {
23 fn default() -> Self {
24 Self {
25 resolution_weight: WEIGHT_RESOLUTION,
26 video_codec_weight: WEIGHT_VIDEO_CODEC,
27 audio_codec_weight: WEIGHT_AUDIO_CODEC,
28 source_weight: WEIGHT_SOURCE,
29 group_trust_weight: WEIGHT_GROUP_TRUST,
30 }
31 }
32}
33
34impl QualityProfile {
35 #[must_use]
37 pub fn is_valid(&self) -> bool {
38 let sum = self.resolution_weight
39 + self.video_codec_weight
40 + self.audio_codec_weight
41 + self.source_weight
42 + self.group_trust_weight;
43 (sum - 1.0).abs() < 0.01
44 }
45}
46
47#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49pub struct QualityScores {
50 pub resolution: Option<f32>,
52 pub video_codec: Option<f32>,
54 pub audio_codec: Option<f32>,
56 pub source: Option<f32>,
58 pub group_trust: f32,
60}
61
62impl QualityScores {
63 #[must_use]
65 pub fn from_metadata(
66 resolution: Option<Resolution>,
67 video_codec: Option<VideoCodec>,
68 audio_codec: Option<AudioCodec>,
69 source: Option<MediaSource>,
70 group_trust: f32,
71 ) -> Self {
72 Self {
73 resolution: resolution.map(|r| r.score()),
74 video_codec: video_codec.map(|v| v.score()),
75 audio_codec: audio_codec.map(|a| a.score()),
76 source: source.map(|s| s.score()),
77 group_trust,
78 }
79 }
80
81 #[must_use]
85 pub fn compute(&self, profile: &QualityProfile) -> f32 {
86 let res = self.resolution.unwrap_or(0.5);
87 let vc = self.video_codec.unwrap_or(0.5);
88 let ac = self.audio_codec.unwrap_or(0.5);
89 let src = self.source.unwrap_or(0.5);
90
91 profile.resolution_weight * res
92 + profile.video_codec_weight * vc
93 + profile.audio_codec_weight * ac
94 + profile.source_weight * src
95 + profile.group_trust_weight * self.group_trust
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn default_profile_is_valid() {
105 let profile = QualityProfile::default();
106 assert!(profile.is_valid());
107 }
108
109 #[test]
110 fn invalid_profile_detected() {
111 let profile = QualityProfile {
112 resolution_weight: 0.5,
113 video_codec_weight: 0.5,
114 audio_codec_weight: 0.5,
115 source_weight: 0.5,
116 group_trust_weight: 0.5,
117 };
118 assert!(!profile.is_valid());
119 }
120
121 #[test]
122 fn quality_scores_full_metadata() {
123 let scores = QualityScores::from_metadata(
124 Some(Resolution::FHD1080),
125 Some(VideoCodec::HEVC),
126 Some(AudioCodec::FLAC),
127 Some(MediaSource::BluRay),
128 0.8,
129 );
130 let profile = QualityProfile::default();
131 let score = scores.compute(&profile);
132
133 let expected = 0.35 * 0.85 + 0.25 * 0.85 + 0.15 * 0.95 + 0.15 * 0.90 + 0.10 * 0.8;
136 assert!(
137 (score - expected).abs() < 0.001,
138 "score={score}, expected={expected}"
139 );
140 }
141
142 #[test]
143 fn quality_scores_missing_metadata_uses_neutral() {
144 let scores = QualityScores::from_metadata(None, None, None, None, 0.5);
145 let profile = QualityProfile::default();
146 let score = scores.compute(&profile);
147 assert!((score - 0.5).abs() < 0.001);
149 }
150
151 #[test]
152 fn quality_scores_partial_metadata() {
153 let scores = QualityScores::from_metadata(
154 Some(Resolution::UHD2160),
155 None,
156 None,
157 Some(MediaSource::BluRayRemux),
158 0.9,
159 );
160 let profile = QualityProfile::default();
161 let score = scores.compute(&profile);
162
163 let expected = 0.35 * 1.0 + 0.25 * 0.5 + 0.15 * 0.5 + 0.15 * 1.0 + 0.10 * 0.9;
164 assert!(
165 (score - expected).abs() < 0.001,
166 "score={score}, expected={expected}"
167 );
168 }
169}