deviantart/types/
media.rs1use std::collections::HashMap;
2use std::fmt::Write;
3use url::Url;
4
5#[derive(Debug, thiserror::Error)]
6pub enum GetFullviewUrlError {
7 #[error("missing base uri")]
8 MissingBaseUri,
9 #[error("missing media type")]
10 MissingMediaType,
11 #[error("unexpected path part \"{path_part}\"")]
12 UnexpectedPathPart { path_part: String },
13 #[error("expected {expected}, found \"{actual}\"")]
14 InvalidPathPart {
15 actual: String,
16 expected: &'static str,
17 },
18 #[error("missing path part")]
19 MissingPathPart,
20 #[error("missing pretty name")]
21 MissingPrettyName,
22 #[error("missing pretty name template")]
23 MissingPrettyNameTemplate,
24 #[error("invalid fullview option format")]
25 InvalidFullviewOptionFormat,
26 #[error("invalid fullview option \"{option}\"")]
27 InvalidFullviewOption { option: String },
28 #[error("duplicate fullview option \"{option}\"")]
29 DuplicateFullviewOption { option: String },
30 #[error("the fullview url is not for a jpg")]
31 NotJpg,
32}
33
34#[derive(Debug, Clone)]
35pub struct GetFullviewUrlOptions {
36 pub quality: Option<u8>,
37 pub strp: Option<bool>,
38 pub png: bool,
39}
40
41impl GetFullviewUrlOptions {
42 pub fn new() -> Self {
43 Self {
44 quality: None,
45 strp: None,
46 png: false,
47 }
48 }
49}
50
51impl Default for GetFullviewUrlOptions {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57fn create_fullview_path(
58 path: &str,
59 pretty_name: &str,
60 options: GetFullviewUrlOptions,
61 path_segments_mut: &mut url::PathSegmentsMut,
62) -> Result<(), GetFullviewUrlError> {
63 let mut path_iter = path.split('/').filter(|p| !p.is_empty());
65
66 {
67 let path_part = path_iter
68 .next()
69 .ok_or(GetFullviewUrlError::MissingPathPart)?;
70 if path_part != "v1" {
71 return Err(GetFullviewUrlError::InvalidPathPart {
72 actual: path_part.to_string(),
73 expected: "v1",
74 });
75 }
76 path_segments_mut.push(path_part);
77 }
78
79 {
80 let path_part = path_iter
81 .next()
82 .ok_or(GetFullviewUrlError::MissingPathPart)?;
83 if path_part != "fit" && path_part != "fill" {
84 return Err(GetFullviewUrlError::InvalidPathPart {
85 actual: path_part.to_string(),
86 expected: "\"fit\" or \"fill\"",
87 });
88 }
89 path_segments_mut.push(path_part);
90 }
91
92 {
93 let path_part = path_iter
94 .next()
95 .ok_or(GetFullviewUrlError::MissingPathPart)?;
96 let mut width: Option<u32> = None;
97 let mut height: Option<u32> = None;
98 let mut quality: Option<u8> = None;
99 let mut strp = false;
100 for part in path_part.split(",") {
101 if part == "strp" {
102 strp = true;
103 continue;
104 }
105
106 let (name, value) = part
107 .split_once('_')
108 .ok_or(GetFullviewUrlError::InvalidFullviewOptionFormat)?;
109 match name {
110 "w" => {
111 if width.is_some() {
112 return Err(GetFullviewUrlError::DuplicateFullviewOption {
113 option: name.to_string(),
114 });
115 }
116 width = Some(value.parse().map_err(|_err| {
117 GetFullviewUrlError::InvalidFullviewOption {
118 option: name.to_string(),
119 }
120 })?);
121 }
122 "h" => {
123 if height.is_some() {
124 return Err(GetFullviewUrlError::DuplicateFullviewOption {
125 option: name.to_string(),
126 });
127 }
128 height = Some(value.parse().map_err(|_err| {
129 GetFullviewUrlError::InvalidFullviewOption {
130 option: name.to_string(),
131 }
132 })?);
133 }
134 "q" => {
135 if quality.is_some() {
136 return Err(GetFullviewUrlError::DuplicateFullviewOption {
137 option: name.to_string(),
138 });
139 }
140 quality = Some(value.parse().map_err(|_err| {
141 GetFullviewUrlError::InvalidFullviewOption {
142 option: name.to_string(),
143 }
144 })?);
145 }
146 _ => {
147 return Err(GetFullviewUrlError::InvalidFullviewOption {
148 option: name.to_string(),
149 });
150 }
151 }
152 }
153 let mut new_path_part = String::new();
154 if let Some(width) = width {
155 write!(&mut new_path_part, "w_{width}").unwrap();
156 }
157 if let Some(height) = height {
158 if !new_path_part.is_empty() {
159 new_path_part.push(',');
160 }
161 write!(&mut new_path_part, "h_{height}").unwrap();
162 }
163 if let Some(quality) = options.quality.or(quality) {
164 if !new_path_part.is_empty() {
165 new_path_part.push(',');
166 }
167 write!(&mut new_path_part, "q_{quality}").unwrap();
168 }
169 if options.strp.unwrap_or(strp) {
170 if !new_path_part.is_empty() {
171 new_path_part.push(',');
172 }
173 new_path_part.push_str("strp");
174 }
175 path_segments_mut.push(&new_path_part);
176 }
177
178 {
179 const TEMPLATE: &str = "<prettyName>";
180
181 let path_part = path_iter
182 .next()
183 .ok_or(GetFullviewUrlError::MissingPathPart)?;
184 if !path_part.contains(TEMPLATE) {
185 return Err(GetFullviewUrlError::MissingPrettyNameTemplate);
186 }
187 let mut path_part = path_part.replace(TEMPLATE, pretty_name);
189
190 if options.png {
192 let path_part_stem = path_part
193 .strip_suffix(".jpg")
194 .ok_or(GetFullviewUrlError::NotJpg)?;
195 path_part = format!("{path_part_stem}.png");
196 }
197
198 path_segments_mut.push(&path_part);
199 }
200
201 {
202 let path_part = path_iter.next();
203 if let Some(path_part) = path_part {
204 return Err(GetFullviewUrlError::UnexpectedPathPart {
205 path_part: path_part.to_string(),
206 });
207 }
208 }
209
210 Ok(())
211}
212
213#[derive(Debug, serde::Deserialize)]
215pub struct MediaType {
216 #[serde(rename = "c")]
218 pub content: Option<String>,
219
220 #[serde(rename = "h")]
222 pub height: u64,
223
224 #[serde(rename = "t")]
228 pub kind: String,
229
230 #[serde(rename = "w")]
232 pub width: u64,
233
234 pub b: Option<Url>,
238
239 #[serde(flatten)]
241 pub unknown: HashMap<String, serde_json::Value>,
242}
243
244impl MediaType {
245 pub fn is_fullview(&self) -> bool {
247 self.kind == "fullview"
248 }
249
250 pub fn is_gif(&self) -> bool {
252 self.kind == "gif"
253 }
254
255 pub fn is_video(&self) -> bool {
257 self.kind == "video"
258 }
259}
260
261#[derive(Debug, serde::Deserialize)]
265pub struct Media {
266 #[serde(rename = "baseUri")]
268 pub base_uri: Option<Url>,
269
270 #[serde(default)]
272 pub token: Vec<String>,
273
274 pub types: Vec<MediaType>,
276
277 #[serde(rename = "prettyName")]
279 pub pretty_name: Option<String>,
280
281 #[serde(flatten)]
283 pub unknown: HashMap<String, serde_json::Value>,
284}
285
286impl Media {
287 pub fn get_fullview_media_type(&self) -> Option<&MediaType> {
289 self.types.iter().find(|t| t.is_fullview())
290 }
291
292 pub fn get_gif_media_type(&self) -> Option<&MediaType> {
294 self.types.iter().find(|t| t.is_gif())
295 }
296
297 pub fn get_best_video_media_type(&self) -> Option<&MediaType> {
299 self.types
300 .iter()
301 .filter(|media_type| media_type.is_video())
302 .max_by_key(|media_type| media_type.width)
303 }
304
305 pub fn get_fullview_url(
307 &self,
308 options: GetFullviewUrlOptions,
309 ) -> Result<Url, GetFullviewUrlError> {
310 let mut url = self
311 .base_uri
312 .as_ref()
313 .ok_or(GetFullviewUrlError::MissingBaseUri)?
314 .clone();
315
316 if let Some(path) = self
318 .get_fullview_media_type()
319 .ok_or(GetFullviewUrlError::MissingMediaType)?
320 .content
321 .as_ref()
322 {
323 let mut path_segments_mut = url
324 .path_segments_mut()
325 .ok()
326 .ok_or(GetFullviewUrlError::MissingPathPart)?;
327
328 let pretty_name = self
329 .pretty_name
330 .as_ref()
331 .ok_or(GetFullviewUrlError::MissingPrettyName)?;
332 create_fullview_path(path, pretty_name, options, &mut path_segments_mut)?;
333 }
334
335 if let Some(token) = self.token.first() {
340 url.query_pairs_mut().append_pair("token", token);
341 }
342
343 Ok(url)
344 }
345}