You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
411 lines
13 KiB
Rust
411 lines
13 KiB
Rust
use crate::api_core::common::{
|
|
FileIdentifier, FileRecord, FileSelection, FileServiceSelection, ServiceIdentifier,
|
|
};
|
|
use crate::api_core::endpoints::adding_tags::{AddTagsRequestBuilder, TagAction};
|
|
use crate::api_core::endpoints::searching_and_fetching_files::{FileFullMetadata, FullMetadata};
|
|
use crate::error::{Error, Result};
|
|
use crate::utils::tag_list_to_string_list;
|
|
use crate::wrapper::builders::delete_files_builder::DeleteFilesBuilder;
|
|
use crate::wrapper::builders::notes_builder::AddNotesBuilder;
|
|
use crate::wrapper::service::ServiceName;
|
|
use crate::wrapper::tag::Tag;
|
|
use crate::Client;
|
|
use chrono::{NaiveDateTime, TimeZone, Utc};
|
|
use mime::Mime;
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
|
pub enum FileStatus {
|
|
ReadyForImport,
|
|
InDatabase,
|
|
Deleted,
|
|
Unknown,
|
|
}
|
|
|
|
impl Eq for FileStatus {}
|
|
|
|
impl From<u8> for FileStatus {
|
|
fn from(v: u8) -> FileStatus {
|
|
match v {
|
|
3 => FileStatus::Deleted,
|
|
0 => FileStatus::ReadyForImport,
|
|
_ => FileStatus::InDatabase,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct HydrusFile {
|
|
pub(crate) client: Client,
|
|
pub id: FileIdentifier,
|
|
pub status: FileStatus,
|
|
pub(crate) metadata: Option<FileFullMetadata>,
|
|
}
|
|
|
|
impl HydrusFile {
|
|
pub(crate) fn from_id(client: Client, id: u64) -> Self {
|
|
Self {
|
|
client,
|
|
id: FileIdentifier::ID(id),
|
|
status: FileStatus::Unknown,
|
|
metadata: None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn from_raw_status_and_hash<S: ToString>(
|
|
client: Client,
|
|
status: u8,
|
|
hash: S,
|
|
) -> Self {
|
|
Self {
|
|
client,
|
|
id: FileIdentifier::Hash(hash.to_string()),
|
|
status: status.into(),
|
|
metadata: None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn from_metadata(client: Client, metadata: FileFullMetadata) -> Self {
|
|
let status = if metadata.is_trashed {
|
|
FileStatus::Deleted
|
|
} else {
|
|
FileStatus::InDatabase
|
|
};
|
|
|
|
Self {
|
|
client,
|
|
id: FileIdentifier::Hash(metadata.basic_metadata.identifiers.hash.clone()),
|
|
status,
|
|
metadata: Some(metadata),
|
|
}
|
|
}
|
|
|
|
/// Deletes the internally stored metadata about the file retrieves it again
|
|
pub async fn update(&mut self) -> Result<()> {
|
|
self.metadata = None;
|
|
self.metadata().await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns the hash of the file
|
|
/// if the file identifier is an id it calls hydrus to resolve the file
|
|
pub async fn hash(&mut self) -> Result<String> {
|
|
match &self.id {
|
|
FileIdentifier::ID(_) => {
|
|
let metadata = self.metadata().await?;
|
|
Ok(metadata.basic_metadata.identifiers.hash.clone())
|
|
}
|
|
FileIdentifier::Hash(hash) => Ok(hash.clone()),
|
|
}
|
|
}
|
|
|
|
/// Returns the file size in bytes
|
|
pub async fn size(&mut self) -> Result<Option<u64>> {
|
|
let metadata = self.metadata().await?;
|
|
|
|
Ok(metadata.basic_metadata.size.clone())
|
|
}
|
|
|
|
/// Returns the mime of the file
|
|
pub async fn mime(&mut self) -> Result<Mime> {
|
|
let metadata = self.metadata().await?;
|
|
let mime = metadata
|
|
.basic_metadata
|
|
.mime
|
|
.as_str()
|
|
.parse()
|
|
.map_err(|_| Error::InvalidMime(metadata.basic_metadata.mime.clone()))?;
|
|
|
|
Ok(mime)
|
|
}
|
|
|
|
/// Return the file extension
|
|
pub async fn ext(&mut self) -> Result<String> {
|
|
let metadata = self.metadata().await?;
|
|
|
|
Ok(metadata.basic_metadata.ext.clone())
|
|
}
|
|
|
|
/// Returns the dimensions of the file in pixels
|
|
pub async fn dimensions(&mut self) -> Result<Option<(u32, u32)>> {
|
|
let metadata = self.metadata().await?;
|
|
if let (Some(width), Some(height)) = (
|
|
&metadata.basic_metadata.width,
|
|
&metadata.basic_metadata.height,
|
|
) {
|
|
Ok(Some((*width, *height)))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// Returns the duration of the file in seconds if it's a video
|
|
pub async fn duration(&mut self) -> Result<Option<u64>> {
|
|
let metadata = self.metadata().await?;
|
|
|
|
Ok(metadata.basic_metadata.duration.clone())
|
|
}
|
|
|
|
/// Returns the number of frames of the file if it's a video
|
|
pub async fn num_frames(&mut self) -> Result<Option<u64>> {
|
|
let metadata = self.metadata().await?;
|
|
|
|
Ok(metadata.basic_metadata.num_frames.clone())
|
|
}
|
|
|
|
/// Returns if the file has audio
|
|
pub async fn has_audio(&mut self) -> Result<bool> {
|
|
let metadata = self.metadata().await?;
|
|
|
|
Ok(metadata.basic_metadata.has_audio.unwrap_or(false))
|
|
}
|
|
|
|
/// Returns if the file is currently in the inbox
|
|
pub async fn in_inbox(&mut self) -> Result<bool> {
|
|
let metadata = self.metadata().await?;
|
|
|
|
Ok(metadata.is_inbox)
|
|
}
|
|
|
|
/// Returns if the file is stored locally
|
|
pub async fn stored_locally(&mut self) -> Result<bool> {
|
|
let metadata = self.metadata().await?;
|
|
|
|
Ok(metadata.is_local)
|
|
}
|
|
|
|
/// Returns if the file has been moved to the trash
|
|
pub async fn moved_to_trashed(&mut self) -> Result<bool> {
|
|
let metadata = self.metadata().await?;
|
|
|
|
Ok(metadata.is_trashed)
|
|
}
|
|
|
|
/// Returns all urls associated with the file
|
|
pub async fn urls(&mut self) -> Result<&Vec<String>> {
|
|
let metadata = self.metadata().await?;
|
|
|
|
Ok(&metadata.known_urls)
|
|
}
|
|
|
|
/// Returns the modified time of the file
|
|
pub async fn time_modified(&mut self) -> Result<Option<NaiveDateTime>> {
|
|
let metadata = self.metadata().await?;
|
|
let naive_time_modified = metadata
|
|
.basic_metadata
|
|
.time_modified
|
|
.map(|m| Utc.timestamp_millis(m as i64).naive_utc());
|
|
|
|
Ok(naive_time_modified)
|
|
}
|
|
|
|
/// Returns the imported time of the file for a given file service key
|
|
pub async fn time_imported<S: AsRef<str>>(
|
|
&mut self,
|
|
service_key: S,
|
|
) -> Result<Option<NaiveDateTime>> {
|
|
let metadata = self.metadata().await?;
|
|
let naive_time_imported = metadata
|
|
.file_services
|
|
.current
|
|
.get(service_key.as_ref())
|
|
.map(|s| s.time_imported)
|
|
.or_else(|| {
|
|
metadata
|
|
.file_services
|
|
.deleted
|
|
.get(service_key.as_ref())
|
|
.map(|s| s.time_imported)
|
|
})
|
|
.map(|millis| Utc.timestamp_millis(millis as i64).naive_utc());
|
|
|
|
Ok(naive_time_imported)
|
|
}
|
|
|
|
/// Returns the time the file was deleted for a specified file service
|
|
pub async fn time_deleted<S: AsRef<str>>(
|
|
&mut self,
|
|
service_key: S,
|
|
) -> Result<Option<NaiveDateTime>> {
|
|
let metadata = self.metadata().await?;
|
|
let naive_time_deleted = metadata
|
|
.file_services
|
|
.deleted
|
|
.get(service_key.as_ref())
|
|
.map(|service| service.time_deleted)
|
|
.map(|millis| Utc.timestamp_millis(millis as i64).naive_utc());
|
|
|
|
Ok(naive_time_deleted)
|
|
}
|
|
|
|
/// Creates a request builder to delete the file
|
|
pub fn delete(&mut self) -> DeleteFilesBuilder {
|
|
self.metadata = None;
|
|
DeleteFilesBuilder::new(self.client.clone()).add_file(self.id.clone())
|
|
}
|
|
|
|
/// Undeletes the file for the given service or all services
|
|
/// if `FileServiceSelection::none` is passed
|
|
pub async fn undelete(&mut self, service_selection: FileServiceSelection) -> Result<()> {
|
|
let hash = self.hash().await?;
|
|
self.metadata = None;
|
|
self.client
|
|
.undelete_files(FileSelection::by_hash(hash), service_selection)
|
|
.await
|
|
}
|
|
|
|
/// Archives the file in all passed file services or all configured services
|
|
/// if no selection is passed
|
|
pub async fn archive(&mut self, service_selection: FileServiceSelection) -> Result<()> {
|
|
let hash = self.hash().await?;
|
|
self.metadata = None;
|
|
self.client
|
|
.archive_files(FileSelection::by_hash(hash), service_selection)
|
|
.await
|
|
}
|
|
|
|
/// Unarchives the file for the given services
|
|
pub async fn unarchive(&mut self, service_selection: FileServiceSelection) -> Result<()> {
|
|
let hash = self.hash().await?;
|
|
self.metadata = None;
|
|
self.client
|
|
.unarchive_files(FileSelection::by_hash(hash), service_selection)
|
|
.await
|
|
}
|
|
|
|
/// Associates the file with a list of urls
|
|
pub async fn associate_urls(&mut self, urls: Vec<String>) -> Result<()> {
|
|
let hash = self.hash().await?;
|
|
self.client.associate_urls(urls, vec![hash]).await
|
|
}
|
|
|
|
/// Disassociates the file with a list of urls
|
|
pub async fn disassociate_urls(&mut self, urls: Vec<String>) -> Result<()> {
|
|
let hash = self.hash().await?;
|
|
self.client.disassociate_urls(urls, vec![hash]).await
|
|
}
|
|
|
|
/// Returns map mapping lists of tags to services.
|
|
///
|
|
/// Deprecation: Use [HydrusFile::services_with_tags] instead.
|
|
#[deprecated(note = "Deprecated in the official API. Use services_with_tags instead.")]
|
|
pub async fn service_names_with_tags(&mut self) -> Result<HashMap<ServiceName, Vec<Tag>>> {
|
|
let metadata = self.metadata().await?;
|
|
let mut tag_mappings = HashMap::new();
|
|
|
|
#[allow(deprecated)]
|
|
for (service, status_tags) in &metadata.service_names_to_statuses_to_tags {
|
|
let mut tag_list = Vec::new();
|
|
|
|
for (_, tags) in status_tags {
|
|
tag_list.append(&mut tags.into_iter().map(|t| t.into()).collect())
|
|
}
|
|
tag_mappings.insert(ServiceName(service.clone()), tag_list);
|
|
}
|
|
|
|
Ok(tag_mappings)
|
|
}
|
|
|
|
/// Returns a mapping with service ids mapped to tags
|
|
pub async fn services_with_tags(&mut self) -> Result<HashMap<ServiceIdentifier, Vec<Tag>>> {
|
|
let metadata = self.metadata().await?;
|
|
let mut tag_mappings = HashMap::new();
|
|
|
|
for (service, status_tags) in &metadata.service_keys_to_statuses_to_tags {
|
|
let mut tag_list = Vec::new();
|
|
|
|
for (_, tags) in status_tags {
|
|
tag_list.append(&mut tags.into_iter().map(|t| t.into()).collect())
|
|
}
|
|
tag_mappings.insert(ServiceIdentifier::Key(service.clone()), tag_list);
|
|
}
|
|
|
|
Ok(tag_mappings)
|
|
}
|
|
|
|
/// Returns a list of all tags assigned to the file
|
|
pub async fn tags(&mut self) -> Result<Vec<Tag>> {
|
|
let mut tag_list = Vec::new();
|
|
let tag_mappings = self.services_with_tags().await?;
|
|
|
|
for (_, mut tags) in tag_mappings {
|
|
tag_list.append(&mut tags);
|
|
}
|
|
|
|
Ok(tag_list)
|
|
}
|
|
|
|
/// Adds tags for a specific service to the file
|
|
pub async fn add_tags(&mut self, service: ServiceIdentifier, tags: Vec<Tag>) -> Result<()> {
|
|
let hash = self.hash().await?;
|
|
let request = AddTagsRequestBuilder::default()
|
|
.add_hash(hash)
|
|
.add_tags(service, tag_list_to_string_list(tags))
|
|
.build();
|
|
|
|
self.client.add_tags(request).await
|
|
}
|
|
|
|
/// Allows modification of tags by using the defined tag actions
|
|
pub async fn modify_tags(
|
|
&mut self,
|
|
service: ServiceIdentifier,
|
|
action: TagAction,
|
|
tags: Vec<Tag>,
|
|
) -> Result<()> {
|
|
let hash = self.hash().await?;
|
|
let mut reqwest = AddTagsRequestBuilder::default().add_hash(hash);
|
|
|
|
for tag in tags {
|
|
reqwest = reqwest.add_tag_with_action(service.clone(), tag.to_string(), action.clone());
|
|
}
|
|
|
|
self.client.add_tags(reqwest.build()).await
|
|
}
|
|
|
|
/// Creates a builder to add notes to the file
|
|
pub fn add_notes(&self) -> AddNotesBuilder {
|
|
AddNotesBuilder::new(self.client.clone(), self.id.clone())
|
|
}
|
|
|
|
/// Deletes a single note from the file
|
|
pub async fn delete_note<S1: ToString>(&self, name: S1) -> Result<()> {
|
|
self.client
|
|
.delete_notes(self.id.clone(), vec![name.to_string()])
|
|
.await
|
|
}
|
|
|
|
/// Deletes multiple notes from the file
|
|
pub async fn delete_notes<I: IntoIterator<Item = S>, S: ToString>(
|
|
&self,
|
|
names: I,
|
|
) -> Result<()> {
|
|
let names = names.into_iter().map(|n: S| n.to_string()).collect();
|
|
self.client.delete_notes(self.id.clone(), names).await
|
|
}
|
|
|
|
/// Retrieves the file record bytes
|
|
pub async fn retrieve(&self) -> Result<FileRecord> {
|
|
self.client.get_file(self.id.clone()).await
|
|
}
|
|
|
|
/// Returns the metadata for the given file
|
|
/// if there's already known metadata about the file it uses that
|
|
async fn metadata(&mut self) -> Result<&FileFullMetadata> {
|
|
if self.metadata.is_none() {
|
|
let metadata = self
|
|
.client
|
|
.get_file_metadata_by_identifier::<FullMetadata>(self.id.clone())
|
|
.await?;
|
|
self.status = if metadata.is_trashed {
|
|
FileStatus::Deleted
|
|
} else {
|
|
FileStatus::InDatabase
|
|
};
|
|
self.metadata = Some(metadata);
|
|
}
|
|
|
|
Ok(self.metadata.as_ref().unwrap())
|
|
}
|
|
}
|