hydrus_api/wrapper/
hydrus_file.rs

1use crate::api_core::common::{
2    FileIdentifier, FileRecord, FileSelection, FileServiceSelection, ServiceIdentifier,
3};
4use crate::api_core::endpoints::adding_tags::{AddTagsRequestBuilder, TagAction};
5use crate::api_core::endpoints::searching_and_fetching_files::{FileFullMetadata, FullMetadata};
6use crate::error::{Error, Result};
7use crate::utils::tag_list_to_string_list;
8use crate::wrapper::builders::delete_files_builder::DeleteFilesBuilder;
9use crate::wrapper::builders::notes_builder::AddNotesBuilder;
10
11use crate::wrapper::tag::Tag;
12use crate::Client;
13use chrono::{NaiveDateTime, TimeZone, Utc};
14use mime::Mime;
15use std::collections::HashMap;
16
17#[derive(Clone, Debug, PartialOrd, PartialEq)]
18pub enum FileStatus {
19    ReadyForImport,
20    InDatabase,
21    Deleted,
22    Unknown,
23}
24
25impl Eq for FileStatus {}
26
27impl From<u8> for FileStatus {
28    fn from(v: u8) -> FileStatus {
29        match v {
30            3 => FileStatus::Deleted,
31            0 => FileStatus::ReadyForImport,
32            _ => FileStatus::InDatabase,
33        }
34    }
35}
36
37#[derive(Clone)]
38pub struct HydrusFile {
39    pub(crate) client: Client,
40    pub id: FileIdentifier,
41    pub status: FileStatus,
42    pub(crate) metadata: Option<FileFullMetadata>,
43}
44
45impl HydrusFile {
46    pub(crate) fn from_hash<S: ToString>(client: Client, hash: S) -> Self {
47        Self {
48            client,
49            id: FileIdentifier::Hash(hash.to_string()),
50            status: FileStatus::Unknown,
51            metadata: None,
52        }
53    }
54
55    pub(crate) fn from_raw_status_and_hash<S: ToString>(
56        client: Client,
57        status: u8,
58        hash: S,
59    ) -> Self {
60        Self {
61            client,
62            id: FileIdentifier::Hash(hash.to_string()),
63            status: status.into(),
64            metadata: None,
65        }
66    }
67
68    pub(crate) fn from_metadata(client: Client, metadata: FileFullMetadata) -> Self {
69        let status = if metadata.is_trashed {
70            FileStatus::Deleted
71        } else {
72            FileStatus::InDatabase
73        };
74
75        Self {
76            client,
77            id: FileIdentifier::Hash(metadata.basic_metadata.identifiers.hash.clone()),
78            status,
79            metadata: Some(metadata),
80        }
81    }
82
83    /// Deletes the internally stored metadata about the file retrieves it again
84    pub async fn update(&mut self) -> Result<()> {
85        self.metadata = None;
86        self.metadata().await?;
87        Ok(())
88    }
89
90    /// Returns the hash of the file
91    /// if the file identifier is an id it calls hydrus to resolve the file
92    pub async fn hash(&mut self) -> Result<String> {
93        match &self.id {
94            FileIdentifier::ID(_) => {
95                let metadata = self.metadata().await?;
96                Ok(metadata.basic_metadata.identifiers.hash.clone())
97            }
98            FileIdentifier::Hash(hash) => Ok(hash.clone()),
99        }
100    }
101
102    /// Returns the file size in bytes
103    pub async fn size(&mut self) -> Result<Option<u64>> {
104        let metadata = self.metadata().await?;
105
106        Ok(metadata.basic_metadata.size.clone())
107    }
108
109    /// Returns the mime of the file
110    pub async fn mime(&mut self) -> Result<Mime> {
111        let metadata = self.metadata().await?;
112        let mime = metadata
113            .basic_metadata
114            .mime
115            .as_str()
116            .parse()
117            .map_err(|_| Error::InvalidMime(metadata.basic_metadata.mime.clone()))?;
118
119        Ok(mime)
120    }
121
122    /// Return the file extension
123    pub async fn ext(&mut self) -> Result<String> {
124        let metadata = self.metadata().await?;
125
126        Ok(metadata.basic_metadata.ext.clone())
127    }
128
129    /// Returns the dimensions of the file in pixels
130    pub async fn dimensions(&mut self) -> Result<Option<(u32, u32)>> {
131        let metadata = self.metadata().await?;
132        if let (Some(width), Some(height)) = (
133            &metadata.basic_metadata.width,
134            &metadata.basic_metadata.height,
135        ) {
136            Ok(Some((*width, *height)))
137        } else {
138            Ok(None)
139        }
140    }
141
142    /// Returns the duration of the file in seconds if it's a video
143    pub async fn duration(&mut self) -> Result<Option<u64>> {
144        let metadata = self.metadata().await?;
145
146        Ok(metadata.basic_metadata.duration.clone())
147    }
148
149    /// Returns the number of frames of the file if it's a video
150    pub async fn num_frames(&mut self) -> Result<Option<u64>> {
151        let metadata = self.metadata().await?;
152
153        Ok(metadata.basic_metadata.num_frames.clone())
154    }
155
156    /// Returns if the file has audio
157    pub async fn has_audio(&mut self) -> Result<bool> {
158        let metadata = self.metadata().await?;
159
160        Ok(metadata.basic_metadata.has_audio.unwrap_or(false))
161    }
162
163    /// Returns if the file is currently in the inbox
164    pub async fn in_inbox(&mut self) -> Result<bool> {
165        let metadata = self.metadata().await?;
166
167        Ok(metadata.is_inbox)
168    }
169
170    /// Returns if the file is stored locally
171    pub async fn stored_locally(&mut self) -> Result<bool> {
172        let metadata = self.metadata().await?;
173
174        Ok(metadata.is_local)
175    }
176
177    /// Returns if the file has been moved to the trash
178    pub async fn moved_to_trashed(&mut self) -> Result<bool> {
179        let metadata = self.metadata().await?;
180
181        Ok(metadata.is_trashed)
182    }
183
184    /// Returns all urls associated with the file
185    pub async fn urls(&mut self) -> Result<&Vec<String>> {
186        let metadata = self.metadata().await?;
187
188        Ok(&metadata.known_urls)
189    }
190
191    /// Returns the modified time of the file
192    pub async fn time_modified(&mut self) -> Result<Option<NaiveDateTime>> {
193        let metadata = self.metadata().await?;
194        let naive_time_modified = metadata
195            .basic_metadata
196            .time_modified
197            .map(|m| Utc.timestamp_millis(m as i64).naive_utc());
198
199        Ok(naive_time_modified)
200    }
201
202    /// Returns the imported time of the file for a given file service key
203    pub async fn time_imported<S: AsRef<str>>(
204        &mut self,
205        service_key: S,
206    ) -> Result<Option<NaiveDateTime>> {
207        let metadata = self.metadata().await?;
208        let naive_time_imported = metadata
209            .file_services
210            .current
211            .get(service_key.as_ref())
212            .map(|s| s.time_imported)
213            .or_else(|| {
214                metadata
215                    .file_services
216                    .deleted
217                    .get(service_key.as_ref())
218                    .map(|s| s.time_imported)
219            })
220            .map(|millis| Utc.timestamp_millis(millis as i64).naive_utc());
221
222        Ok(naive_time_imported)
223    }
224
225    /// Returns the time the file was deleted for a specified file service
226    pub async fn time_deleted<S: AsRef<str>>(
227        &mut self,
228        service_key: S,
229    ) -> Result<Option<NaiveDateTime>> {
230        let metadata = self.metadata().await?;
231        let naive_time_deleted = metadata
232            .file_services
233            .deleted
234            .get(service_key.as_ref())
235            .map(|service| service.time_deleted)
236            .map(|millis| Utc.timestamp_millis(millis as i64).naive_utc());
237
238        Ok(naive_time_deleted)
239    }
240
241    /// Creates a request builder to delete the file
242    pub fn delete(&mut self) -> DeleteFilesBuilder {
243        self.metadata = None;
244        DeleteFilesBuilder::new(self.client.clone()).add_file(self.id.clone())
245    }
246
247    /// Undeletes the file for the given service or all services
248    /// if `FileServiceSelection::none` is passed
249    pub async fn undelete(&mut self, service_selection: FileServiceSelection) -> Result<()> {
250        let hash = self.hash().await?;
251        self.metadata = None;
252        self.client
253            .undelete_files(FileSelection::by_hash(hash), service_selection)
254            .await
255    }
256
257    /// Archives the file in all passed file services or all configured services
258    /// if no selection is passed
259    pub async fn archive(&mut self, service_selection: FileServiceSelection) -> Result<()> {
260        let hash = self.hash().await?;
261        self.metadata = None;
262        self.client
263            .archive_files(FileSelection::by_hash(hash), service_selection)
264            .await
265    }
266
267    /// Unarchives the file for the given services
268    pub async fn unarchive(&mut self, service_selection: FileServiceSelection) -> Result<()> {
269        let hash = self.hash().await?;
270        self.metadata = None;
271        self.client
272            .unarchive_files(FileSelection::by_hash(hash), service_selection)
273            .await
274    }
275
276    /// Associates the file with a list of urls
277    pub async fn associate_urls(&mut self, urls: Vec<String>) -> Result<()> {
278        let hash = self.hash().await?;
279        self.client.associate_urls(urls, vec![hash]).await
280    }
281
282    /// Disassociates the file with a list of urls
283    pub async fn disassociate_urls(&mut self, urls: Vec<String>) -> Result<()> {
284        let hash = self.hash().await?;
285        self.client.disassociate_urls(urls, vec![hash]).await
286    }
287
288    /// Returns map mapping lists of tags to services.
289    ///
290    /// Deprecation: Use [HydrusFile::services_with_tags] instead.
291    #[deprecated(note = "Deprecated in the official API. Use services_with_tags instead.")]
292    pub async fn service_names_with_tags(
293        &mut self,
294    ) -> Result<HashMap<ServiceIdentifier, Vec<Tag>>> {
295        let metadata = self.metadata().await?;
296        let mut tag_mappings = HashMap::new();
297
298        for (service, service_tags) in &metadata.tags {
299            let mut tag_list = Vec::new();
300
301            for (_, tags) in &service_tags.storage_tags {
302                tag_list.append(&mut tags.into_iter().map(|t| t.into()).collect())
303            }
304            tag_mappings.insert(ServiceIdentifier::Key(service.clone()), tag_list);
305        }
306
307        Ok(tag_mappings)
308    }
309
310    /// Returns a mapping with service ids mapped to tags
311    pub async fn services_with_tags(&mut self) -> Result<HashMap<ServiceIdentifier, Vec<Tag>>> {
312        let metadata = self.metadata().await?;
313        let mut tag_mappings = HashMap::new();
314
315        for (service, service_tags) in &metadata.tags {
316            let mut tag_list = Vec::new();
317
318            for (_, tags) in &service_tags.storage_tags {
319                tag_list.append(&mut tags.into_iter().map(|t| t.into()).collect())
320            }
321            tag_mappings.insert(ServiceIdentifier::Key(service.clone()), tag_list);
322        }
323
324        Ok(tag_mappings)
325    }
326
327    /// Returns a list of all tags assigned to the file
328    pub async fn tags(&mut self) -> Result<Vec<Tag>> {
329        let mut tag_list = Vec::new();
330        let tag_mappings = self.services_with_tags().await?;
331
332        for (_, mut tags) in tag_mappings {
333            tag_list.append(&mut tags);
334        }
335
336        Ok(tag_list)
337    }
338
339    /// Adds tags for a specific service to the file
340    pub async fn add_tags(&mut self, service_key: String, tags: Vec<Tag>) -> Result<()> {
341        let hash = self.hash().await?;
342        let request = AddTagsRequestBuilder::default()
343            .add_hash(hash)
344            .add_tags(service_key, tag_list_to_string_list(tags))
345            .build();
346
347        self.client.add_tags(request).await
348    }
349
350    /// Allows modification of tags by using the defined tag actions
351    pub async fn modify_tags(
352        &mut self,
353        service_key: String,
354        action: TagAction,
355        tags: Vec<Tag>,
356    ) -> Result<()> {
357        let hash = self.hash().await?;
358        let mut reqwest = AddTagsRequestBuilder::default().add_hash(hash);
359
360        for tag in tags {
361            reqwest =
362                reqwest.add_tag_with_action(service_key.clone(), tag.to_string(), action.clone());
363        }
364
365        self.client.add_tags(reqwest.build()).await
366    }
367
368    /// Creates a builder to add notes to the file
369    pub fn add_notes(&self) -> AddNotesBuilder {
370        AddNotesBuilder::new(self.client.clone(), self.id.clone())
371    }
372
373    /// Deletes a single note from the file
374    pub async fn delete_note<S1: ToString>(&self, name: S1) -> Result<()> {
375        self.client
376            .delete_notes(self.id.clone(), vec![name.to_string()])
377            .await
378    }
379
380    /// Deletes multiple notes from the file
381    pub async fn delete_notes<I: IntoIterator<Item = S>, S: ToString>(
382        &self,
383        names: I,
384    ) -> Result<()> {
385        let names = names.into_iter().map(|n: S| n.to_string()).collect();
386        self.client.delete_notes(self.id.clone(), names).await
387    }
388
389    /// Retrieves the file record bytes
390    pub async fn retrieve(&self) -> Result<FileRecord> {
391        self.client.get_file(self.id.clone()).await
392    }
393
394    /// Returns the metadata for the given file
395    /// if there's already known metadata about the file it uses that
396    async fn metadata(&mut self) -> Result<&FileFullMetadata> {
397        if self.metadata.is_none() {
398            let metadata = self
399                .client
400                .get_file_metadata_by_identifier::<FullMetadata>(self.id.clone())
401                .await?;
402            self.status = if metadata.is_trashed {
403                FileStatus::Deleted
404            } else {
405                FileStatus::InDatabase
406            };
407            self.metadata = Some(metadata);
408        }
409
410        Ok(self.metadata.as_ref().unwrap())
411    }
412}