deviantart/types/
media.rs

1use 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    // Parse: /v1/{fill,fit}/w_1280,h_1024,q_80,strp/<prettyName>-fullview.jpg
64    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        // Replace "<pretty-name>" with the actual pretty name.
188        let mut path_part = path_part.replace(TEMPLATE, pretty_name);
189
190        // Make the url a png url if requested.
191        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/// DeviantArt [`DeviationMedia`] media type.
214#[derive(Debug, serde::Deserialize)]
215pub struct MediaType {
216    /// The content. A uri used with base_uri.
217    #[serde(rename = "c")]
218    pub content: Option<String>,
219
220    /// Image height
221    #[serde(rename = "h")]
222    pub height: u64,
223
224    // /// ?
225    // // pub r: u64,
226    /// The kind of media
227    #[serde(rename = "t")]
228    pub kind: String,
229
230    /// Image Width
231    #[serde(rename = "w")]
232    pub width: u64,
233
234    // /// ?
235    // // pub f: Option<u64>,
236    /// ?
237    pub b: Option<Url>,
238
239    /// Unknown K/Vs
240    #[serde(flatten)]
241    pub unknown: HashMap<String, serde_json::Value>,
242}
243
244impl MediaType {
245    /// Whether this is the fullview
246    pub fn is_fullview(&self) -> bool {
247        self.kind == "fullview"
248    }
249
250    /// Whether this is a gif
251    pub fn is_gif(&self) -> bool {
252        self.kind == "gif"
253    }
254
255    /// Whether this is a video
256    pub fn is_video(&self) -> bool {
257        self.kind == "video"
258    }
259}
260
261/// A structure that stores media metadata.
262///
263/// Needed to create image urls.
264#[derive(Debug, serde::Deserialize)]
265pub struct Media {
266    /// The base uri
267    #[serde(rename = "baseUri")]
268    pub base_uri: Option<Url>,
269
270    /// Image token
271    #[serde(default)]
272    pub token: Vec<String>,
273
274    /// Types
275    pub types: Vec<MediaType>,
276
277    /// Pretty Name
278    #[serde(rename = "prettyName")]
279    pub pretty_name: Option<String>,
280
281    /// Unknown K/Vs
282    #[serde(flatten)]
283    pub unknown: HashMap<String, serde_json::Value>,
284}
285
286impl Media {
287    /// Try to get the fullview [`MediaType`].
288    pub fn get_fullview_media_type(&self) -> Option<&MediaType> {
289        self.types.iter().find(|t| t.is_fullview())
290    }
291
292    /// Try to get the gif [`MediaType`].
293    pub fn get_gif_media_type(&self) -> Option<&MediaType> {
294        self.types.iter().find(|t| t.is_gif())
295    }
296
297    /// Try to get the video [`MediaType`]
298    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    /// Get the fullview url for this [`Media`].
306    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        // Allow the "content" section of the path to not exist, but the fullview data MUST exist.
317        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        // We assume that a token is not provided in cases where it is not needed.
336        // As such, this part is optional.
337        // So far, a token is allowed to be missing when the "content" section of the fullview data is missing
338        // Correct this if these assumptions are wrong.
339        if let Some(token) = self.token.first() {
340            url.query_pairs_mut().append_pair("token", token);
341        }
342
343        Ok(url)
344    }
345}