1use std::{cmp::Ordering, collections::BTreeMap, fmt, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{AniError, Result};
6
7#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
8#[serde(rename_all = "lowercase")]
9pub enum CatalogProvider {
10 #[default]
11 Anikoto,
12 Anikoto2,
13}
14
15impl fmt::Display for CatalogProvider {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 f.write_str(match self {
18 Self::Anikoto => "anikoto",
19 Self::Anikoto2 => "anikoto2",
20 })
21 }
22}
23
24impl FromStr for CatalogProvider {
25 type Err = AniError;
26
27 fn from_str(value: &str) -> Result<Self> {
28 match value.to_ascii_lowercase().as_str() {
29 "anikoto" | "anikoto1" | "anikoto-api" => Ok(Self::Anikoto),
30 "anikoto2" | "anikoto-cz" | "anikoto.cz" => Ok(Self::Anikoto2),
31 _ => Err(AniError::Input(format!(
32 "provider must be anikoto or anikoto2, got {value}"
33 ))),
34 }
35 }
36}
37
38#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
39#[serde(rename_all = "lowercase")]
40pub enum TranslationType {
41 #[default]
42 Sub,
43 Dub,
44}
45
46impl fmt::Display for TranslationType {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.write_str(match self {
49 Self::Sub => "sub",
50 Self::Dub => "dub",
51 })
52 }
53}
54
55impl FromStr for TranslationType {
56 type Err = AniError;
57 fn from_str(value: &str) -> Result<Self> {
58 match value.to_ascii_lowercase().as_str() {
59 "sub" => Ok(Self::Sub),
60 "dub" => Ok(Self::Dub),
61 _ => Err(AniError::Input(format!(
62 "translation type must be sub or dub, got {value}"
63 ))),
64 }
65 }
66}
67
68#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
69pub struct SearchOptions {
70 pub allow_adult: bool,
72}
73
74#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
75pub struct SearchResult {
76 pub id: String,
77 pub name: String,
78 pub episodes: f64,
79 #[serde(default)]
80 pub provider: CatalogProvider,
81}
82
83#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
84pub struct SubtitleTrack {
85 pub label: String,
86 pub url: String,
87 #[serde(default)]
88 pub default: bool,
89}
90
91#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
92pub struct RequestHeaders {
93 pub referer: Option<String>,
94 pub origin: Option<String>,
95 #[serde(default)]
96 pub extra: BTreeMap<String, String>,
97}
98
99#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
100pub struct StreamLink {
101 pub url: String,
102 pub resolution: String,
103 pub hls: bool,
104 pub provider: String,
105 pub downloadable: bool,
106 #[serde(default)]
107 pub headers: RequestHeaders,
108 #[serde(default)]
109 pub subtitles: Vec<SubtitleTrack>,
110}
111
112fn resolution_weight(value: &str) -> i32 {
113 let digits: String = value.chars().take_while(|c| c.is_ascii_digit()).collect();
114 digits.parse().unwrap_or_else(|_| {
115 if value.eq_ignore_ascii_case("auto") {
116 -1
117 } else {
118 0
119 }
120 })
121}
122
123fn provider_weight(value: &str) -> i32 {
124 let value = value.to_ascii_lowercase();
125 if value.contains("s-mp4") {
126 3_000
127 } else if value.contains("mp4") {
128 2_000
129 } else if value.contains("default") {
130 1_000
131 } else {
132 0
133 }
134}
135
136pub(crate) fn sort_streams(streams: &mut [StreamLink]) {
137 streams.sort_by(|a, b| {
138 provider_weight(&b.provider)
139 .cmp(&provider_weight(&a.provider))
140 .then_with(|| resolution_weight(&b.resolution).cmp(&resolution_weight(&a.resolution)))
141 .then_with(|| b.hls.cmp(&a.hls))
142 .then_with(|| a.provider.cmp(&b.provider))
143 });
144}
145
146pub fn choose_quality<'a>(streams: &'a [StreamLink], quality: &str) -> Option<&'a StreamLink> {
147 if streams.is_empty() {
148 return None;
149 }
150 match quality.to_ascii_lowercase().as_str() {
151 "best" => streams.first(),
152 "worst" => streams
153 .iter()
154 .filter(|s| resolution_weight(&s.resolution) > 0)
155 .min_by_key(|s| resolution_weight(&s.resolution))
156 .or_else(|| streams.last()),
157 requested => streams
158 .iter()
159 .find(|s| s.resolution.to_ascii_lowercase().contains(requested))
160 .or_else(|| streams.first()),
161 }
162}
163
164fn episode_number(value: &str) -> Option<f64> {
165 value.parse::<f64>().ok().filter(|n| n.is_finite())
166}
167
168pub fn sort_episodes(episodes: &mut [String]) {
169 episodes.sort_by(|a, b| match (episode_number(a), episode_number(b)) {
170 (Some(a), Some(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
171 _ => a.cmp(b),
172 });
173}
174
175pub fn expand_episode_selection(selection: &str, available: &[String]) -> Result<Vec<String>> {
176 let trimmed = selection.trim();
177 if trimmed == "-1" {
178 return available
179 .last()
180 .cloned()
181 .map(|v| vec![v])
182 .ok_or_else(|| AniError::UnavailableNoEpisodes);
183 }
184 if trimmed.contains(char::is_whitespace) {
185 let requested: Vec<_> = trimmed.split_whitespace().map(str::to_owned).collect();
186 if requested.iter().all(|v| available.contains(v)) {
187 return Ok(requested);
188 }
189 eprintln!("One or more selected episodes do not exist");
190 return Err(AniError::InputInvalidEpisode);
191 }
192 if let Some((start, end)) = trimmed.split_once('-') {
193 let end = if end == "-1" || end.is_empty() {
194 available.last().map(String::as_str).unwrap_or("")
195 } else {
196 end
197 };
198 let start_index = available
199 .iter()
200 .position(|v| v == start)
201 .ok_or_else(|| AniError::InputInvalidEpisode)?;
202 let end_index = available
203 .iter()
204 .position(|v| v == end)
205 .ok_or_else(|| AniError::InputInvalidEpisode)?;
206 if start_index > end_index {
207 eprintln!("Episode range is reversed");
208 return Err(AniError::InputInvalidEpisode);
209 }
210 return Ok(available[start_index..=end_index].to_vec());
211 }
212 if available.iter().any(|v| v == trimmed) {
213 Ok(vec![trimmed.to_owned()])
214 } else {
215 Err(AniError::InputInvalidEpisode)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 fn stream(resolution: &str) -> StreamLink {
223 StreamLink {
224 url: resolution.into(),
225 resolution: resolution.into(),
226 hls: false,
227 provider: "Default".into(),
228 downloadable: true,
229 headers: RequestHeaders::default(),
230 subtitles: vec![],
231 }
232 }
233
234 #[test]
235 fn quality_selection_falls_back_to_best() {
236 let streams = vec![stream("1080p"), stream("720p"), stream("480p")];
237 assert_eq!(
238 choose_quality(&streams, "worst").unwrap().resolution,
239 "480p"
240 );
241 assert_eq!(choose_quality(&streams, "720").unwrap().resolution, "720p");
242 assert_eq!(
243 choose_quality(&streams, "1440p").unwrap().resolution,
244 "1080p"
245 );
246 }
247
248 #[test]
249 fn expands_ranges_and_latest() {
250 let eps = vec!["1".into(), "2".into(), "2.5".into(), "3".into()];
251 assert_eq!(
252 expand_episode_selection("2-3", &eps).unwrap(),
253 vec!["2", "2.5", "3"]
254 );
255 assert_eq!(expand_episode_selection("-1", &eps).unwrap(), vec!["3"]);
256 }
257}