summaryrefslogtreecommitdiff
path: root/src/async_lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/async_lib')
-rw-r--r--src/async_lib/client.rs449
-rw-r--r--src/async_lib/traits.rs71
-rw-r--r--src/async_lib/types.rs249
3 files changed, 769 insertions, 0 deletions
diff --git a/src/async_lib/client.rs b/src/async_lib/client.rs
new file mode 100644
index 0000000..58e3b83
--- /dev/null
+++ b/src/async_lib/client.rs
@@ -0,0 +1,449 @@
+use std::{collections::HashMap, path::PathBuf};
+
+use async_trait::async_trait;
+use reqwest::{Body, RequestBuilder};
+use serde::Deserialize;
+use tokio_util::codec::{BytesCodec, FramedRead};
+
+use crate::async_lib::{traits::*, types::*};
+
+type Result<T> = std::result::Result<T, HydrusError>;
+
+/// hydrus client
+pub struct HydrusClient {
+ client: reqwest::Client,
+ apikey: Option<String>,
+ sessionkey: Option<String>,
+ url: String,
+}
+
+impl HydrusClient {
+ /// create a new hydurs client object. requires a url to hydrus API endpoint
+ pub fn new(url: &str) -> HydrusClient {
+ HydrusClient {
+ client: reqwest::Client::new(),
+ apikey: None,
+ sessionkey: None,
+ url: url.to_string(),
+ }
+ }
+ /// set an api key
+ pub fn set_api_key(&mut self, key: &str) {
+ self.apikey = Some(key.to_owned())
+ }
+ /// set a session key
+ pub fn set_session_key(&mut self, key: &str) {
+ self.sessionkey = Some(key.to_owned())
+ }
+
+ fn set_get_request_key(&self, url: &str) -> Result<RequestBuilder> {
+ let request = self.client.get(url);
+ if let Some(key) = &self.sessionkey {
+ Ok(request.header("Hydrus-Client-API-Access-Key", key))
+ } else if let Some(key) = &self.apikey {
+ Ok(request.header("Hydrus-Client-API-Access-Key", key))
+ } else {
+ Err(HydrusError::KeyNotSupplied)
+ }
+ }
+
+ fn set_post_request_key(&self, url: &str) -> Result<RequestBuilder> {
+ let request = self.client.post(url);
+ if let Some(key) = &self.sessionkey {
+ Ok(request.header("Hydrus-Client-API-Access-Key", key))
+ } else if let Some(key) = &self.apikey {
+ Ok(request.header("Hydrus-Client-API-Access-Key", key))
+ } else {
+ Err(HydrusError::KeyNotSupplied)
+ }
+ }
+}
+
+#[derive(Deserialize, Debug)]
+struct HydrusResponse<T> {
+ #[serde(
+ alias = "service",
+ alias = "services",
+ alias = "access_key",
+ alias = "session_key"
+ )]
+ body: T,
+}
+
+#[async_trait]
+impl AccessManagement for HydrusClient {
+ async fn request_new_permissions(
+ &self,
+ name: &str,
+ permissions: &[HydrusPermissions],
+ ) -> Result<String> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("request_new_permissions");
+
+ let mut request = self.client.get(req_url);
+ request = request.query(&[("name", &urlencoding::encode(name))]);
+
+ if permissions.is_empty() {
+ request = request.query(&[("permit_everything", "true")]);
+ } else {
+ let json_string = serde_json::to_string(&permissions)?;
+ request = request.query(&[("basic_permissions", &urlencoding::encode(&json_string))]);
+ };
+
+ Ok(request
+ .send()
+ .await?
+ .json::<HydrusResponse<String>>()
+ .await?
+ .body)
+ }
+
+ async fn get_session_key(&self) -> Result<String> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("session_key");
+
+ let mut request = self.client.get(req_url);
+
+ if let Some(key) = &self.apikey {
+ request = request.header("Hydrus-Client-API-Access-Key", key);
+ }
+
+ Ok(request
+ .send()
+ .await?
+ .json::<HydrusResponse<String>>()
+ .await?
+ .body)
+ }
+
+ async fn verify_access_key(&self, key: &str) -> Result<KeyInfo> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("verify_access_key");
+
+ Ok(self
+ .client
+ .get(req_url)
+ .header("Hydrus-Client-API-Access-Key", key)
+ .send()
+ .await?
+ .json::<KeyInfo>()
+ .await?)
+ }
+
+ async fn get_service_name(&self, name: &str) -> Result<Service> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("get_service");
+ let mut request = self.set_get_request_key(&req_url)?;
+ request = request.query(&[("service_name", &urlencoding::encode(name))]);
+
+ Ok(request
+ .send()
+ .await?
+ .json::<HydrusResponse<Service>>()
+ .await?
+ .body)
+ }
+
+ async fn get_service_key(&self, key: &str) -> Result<Service> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("get_service");
+ let mut request = self.set_get_request_key(&req_url)?;
+ request = request.query(&[("service_key", &urlencoding::encode(key))]);
+
+ Ok(request
+ .send()
+ .await?
+ .json::<HydrusResponse<Service>>()
+ .await?
+ .body)
+ }
+
+ async fn get_services(&self) -> Result<HashMap<String, Service>> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("get_services");
+
+ let mut services = self
+ .set_get_request_key(&req_url)?
+ .send()
+ .await?
+ .json::<HydrusResponse<HashMap<String, Service>>>()
+ .await?
+ .body;
+
+ for (key, service) in services.iter_mut() {
+ service.service_key = key.to_string();
+ }
+
+ Ok(services)
+ }
+}
+
+#[async_trait]
+impl ImportingAndDeletingFiles for HydrusClient {
+ async fn add_file_via_path(
+ &self,
+ path: PathBuf,
+ delete: Option<bool>,
+ domains: Option<FileDomain>,
+ ) -> Result<AddFileResponse> {
+ let mut form = AddFileRequest {
+ path: path.to_owned(),
+ delete_after_successful_import: delete,
+ ..Default::default()
+ };
+
+ if let Some(file_domain) = domains {
+ match file_domain {
+ FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()),
+ FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()),
+ FileDomain::DeletedFileServiceKey(key) => {
+ form.deleted_file_service_key = Some(key.to_string())
+ }
+ FileDomain::DeletedFileServiceKeys(keys) => {
+ form.deleted_file_service_keys = Some(keys.to_owned())
+ }
+ }
+ }
+
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/add_file");
+
+ Ok(self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/json")
+ .json(&form)
+ .send()
+ .await?
+ .json::<AddFileResponse>()
+ .await?)
+ }
+
+ async fn add_file_via_file(&self, file: PathBuf) -> Result<AddFileResponse> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/add_file");
+
+ let file = tokio::fs::File::open(file).await?;
+
+ let stream = FramedRead::new(file, BytesCodec::new());
+ let body = Body::wrap_stream(stream);
+
+ Ok(self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/octet-stream")
+ .body(body)
+ .send()
+ .await?
+ .json::<AddFileResponse>()
+ .await?)
+ }
+
+ async fn delete_files(
+ &self,
+ file: HydrusFile,
+ domain: Option<FileDomain>,
+ reason: Option<String>,
+ ) -> Result<()> {
+ let mut form = FileRequest {
+ file,
+ reason,
+ ..Default::default()
+ };
+
+ if let Some(file_domain) = domain {
+ match file_domain {
+ FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()),
+ FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()),
+ FileDomain::DeletedFileServiceKey(key) => {
+ form.deleted_file_service_key = Some(key.to_string())
+ }
+ FileDomain::DeletedFileServiceKeys(keys) => {
+ form.deleted_file_service_keys = Some(keys.to_owned())
+ }
+ }
+ }
+
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/delete_files");
+
+ let _ = self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/json")
+ .json(&form)
+ .send()
+ .await?;
+
+ Ok(())
+ }
+
+ async fn undelete_files(&self, file: HydrusFile, domain: Option<FileDomain>) -> Result<()> {
+ let mut form = FileRequest {
+ file,
+ ..Default::default()
+ };
+
+ if let Some(file_domain) = domain {
+ match file_domain {
+ FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()),
+ FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()),
+ FileDomain::DeletedFileServiceKey(key) => {
+ form.deleted_file_service_key = Some(key.to_string())
+ }
+ FileDomain::DeletedFileServiceKeys(keys) => {
+ form.deleted_file_service_keys = Some(keys.to_owned())
+ }
+ }
+ }
+
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/undelete_files");
+
+ let _ = self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/json")
+ .json(&form)
+ .send()
+ .await?;
+
+ Ok(())
+ }
+
+ async fn clear_file_deletion_records(&self, file: HydrusFile) -> Result<()> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/clear_file_deletion_record");
+ let _ = self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/json")
+ .json(&file)
+ .send()
+ .await?;
+
+ Ok(())
+ }
+
+ async fn migrate_files(&self, file: HydrusFile, domain: FileDomain) -> Result<()> {
+ let mut form = FileRequest {
+ file,
+ ..Default::default()
+ };
+
+ match domain {
+ FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()),
+ FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()),
+ FileDomain::DeletedFileServiceKey(key) => {
+ form.deleted_file_service_key = Some(key.to_string())
+ }
+ FileDomain::DeletedFileServiceKeys(keys) => {
+ form.deleted_file_service_keys = Some(keys.to_owned())
+ }
+ }
+
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/migrate_files");
+ let _ = self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/json")
+ .json(&form)
+ .send()
+ .await?;
+
+ Ok(())
+ }
+
+ async fn archive_files(&self, file: HydrusFile) -> Result<()> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/archive_files");
+
+ let form = FileRequest {
+ file,
+ ..Default::default()
+ };
+
+ let _ = self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/json")
+ .json(&form)
+ .send()
+ .await?;
+
+ Ok(())
+ }
+
+ async fn unarchive_files(&self, file: HydrusFile) -> Result<()> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/unarchive_files");
+
+ let form = FileRequest {
+ file,
+ ..Default::default()
+ };
+
+ let _ = self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/json")
+ .json(&form)
+ .send()
+ .await?;
+
+ Ok(())
+ }
+
+ async fn generate_hashes_for_path(&self, file: PathBuf) -> Result<HashResponse> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/generate_hashes");
+
+ let form = AddFileRequest {
+ path: file.to_owned(),
+ ..Default::default()
+ };
+
+ Ok(self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/json")
+ .json(&form)
+ .send()
+ .await?
+ .json::<HashResponse>()
+ .await?)
+ }
+
+ async fn generate_hashes_for_file(&self, file: PathBuf) -> Result<HashResponse> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_files/generate_hashes");
+
+ let file = tokio::fs::File::open(file).await?;
+
+ let stream = FramedRead::new(file, BytesCodec::new());
+ let body = Body::wrap_stream(stream);
+
+ Ok(self
+ .set_post_request_key(&req_url)?
+ .header("Content-Type", "application/octet-stream")
+ .body(body)
+ .send()
+ .await?
+ .json::<HashResponse>()
+ .await?)
+ }
+}
+
+#[async_trait]
+impl ImportingAndEditingUrls for HydrusClient {
+ async fn get_url_files(
+ &self,
+ url: &str,
+ doublecheck_file_system: Option<bool>,
+ ) -> Result<FilesUrlResponse> {
+ let mut req_url = self.url.to_owned();
+ req_url.push_str("add_urls/get_url_files");
+
+ let mut request = self.set_get_request_key(&req_url)?;
+
+ request = request.query(&[("url", url)]);
+
+ if let Some(doublecheck) = doublecheck_file_system {
+ request = request.query(&[("doublecheck_file_system", doublecheck)]);
+ }
+
+ Ok(request.send().await?.json::<FilesUrlResponse>().await?)
+ }
+}
diff --git a/src/async_lib/traits.rs b/src/async_lib/traits.rs
new file mode 100644
index 0000000..8177968
--- /dev/null
+++ b/src/async_lib/traits.rs
@@ -0,0 +1,71 @@
+use std::{collections::HashMap, path::PathBuf};
+
+use crate::async_lib::types::*;
+use async_trait::async_trait;
+
+type Result<T> = std::result::Result<T, HydrusError>;
+
+/// Trait for accessing and managing keys and services.
+#[async_trait]
+pub trait AccessManagement {
+ /// Register a new external program with the client. This requires the 'add from api request' mini-dialog under services->review services to be open, otherwise it will 403.
+ async fn request_new_permissions(
+ &self,
+ name: &str,
+ permissions: &[HydrusPermissions],
+ ) -> Result<String>;
+ /// Get a new session key.
+ async fn get_session_key(&self) -> Result<String>;
+ /// Check your access key is valid.
+ async fn verify_access_key(&self, key: &str) -> Result<KeyInfo>;
+ /// Ask the client about a specific service by providing its name.
+ async fn get_service_name(&self, name: &str) -> Result<Service>;
+ /// Ask the client about a specific service by providing its key.
+ async fn get_service_key(&self, key: &str) -> Result<Service>;
+ /// Ask the client about its services.
+ async fn get_services(&self) -> Result<HashMap<String, Service>>;
+}
+
+/// Trait for importing and deleting files.
+#[async_trait]
+pub trait ImportingAndDeletingFiles {
+ /// Tell the client to import a file by providing a local (hydrus-local) file path.
+ async fn add_file_via_path(
+ &self,
+ path: PathBuf,
+ delete: Option<bool>,
+ domains: Option<FileDomain>,
+ ) -> Result<AddFileResponse>;
+ /// Tell the client to import a file by sending the file.
+ async fn add_file_via_file(&self, file: PathBuf) -> Result<AddFileResponse>;
+ /// Tell the client to send files to the trash.
+ async fn delete_files(
+ &self,
+ file: HydrusFile,
+ domain: Option<FileDomain>,
+ reason: Option<String>,
+ ) -> Result<()>;
+ /// Tell the client to restore files that were previously deleted to their old file service(s).
+ async fn undelete_files(&self, file: HydrusFile, domain: Option<FileDomain>) -> Result<()>;
+ /// Tell the client to forget that it once deleted files.
+ async fn clear_file_deletion_records(&self, file: HydrusFile) -> Result<()>;
+ /// Copy files from one local file domain to another.
+ async fn migrate_files(&self, file: HydrusFile, domain: FileDomain) -> Result<()>;
+ /// Tell the client to archive inboxed files.
+ async fn archive_files(&self, file: HydrusFile) -> Result<()>;
+ /// Tell the client re-inbox archived files.
+ async fn unarchive_files(&self, file: HydrusFile) -> Result<()>;
+ /// Generate hashes for an arbitrary file by providing a local path to the file.
+ async fn generate_hashes_for_path(&self, file: PathBuf) -> Result<HashResponse>;
+ /// Generate hashes for an arbitrary file by sending the file.
+ async fn generate_hashes_for_file(&self, file: PathBuf) -> Result<HashResponse>;
+}
+
+#[async_trait]
+pub trait ImportingAndEditingUrls {
+ async fn get_url_files(
+ &self,
+ url: &str,
+ doublecheck_file_system: Option<bool>,
+ ) -> Result<FilesUrlResponse>;
+}
diff --git a/src/async_lib/types.rs b/src/async_lib/types.rs
new file mode 100644
index 0000000..7762065
--- /dev/null
+++ b/src/async_lib/types.rs
@@ -0,0 +1,249 @@
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+use serde_repr::{Deserialize_repr, Serialize_repr};
+use thiserror::Error;
+
+/// Error wrapper
+#[derive(Error, Debug)]
+pub enum HydrusError {
+ #[error("failed to connect to Hydrus")]
+ NetworkError(reqwest::Error),
+ #[error("failed to encode/Deserialize data")]
+ DeserializeError(serde_json::Error),
+ #[error("io error")]
+ IOError(std::io::Error),
+ #[error("api or session key needed")]
+ KeyNotSupplied,
+}
+
+impl From<serde_json::Error> for HydrusError {
+ fn from(value: serde_json::Error) -> Self {
+ HydrusError::DeserializeError(value)
+ }
+}
+
+impl From<std::io::Error> for HydrusError {
+ fn from(value: std::io::Error) -> Self {
+ HydrusError::IOError(value)
+ }
+}
+
+impl From<reqwest::Error> for HydrusError {
+ fn from(value: reqwest::Error) -> Self {
+ HydrusError::NetworkError(value)
+ }
+}
+
+/// Hydrus serivce permissions object
+#[derive(PartialEq, Debug, Clone, Serialize_repr, Deserialize_repr)]
+#[repr(u8)]
+pub enum HydrusPermissions {
+ ImportAndEditURLs = 0,
+ ImportAndEditFiles,
+ EditFileTags,
+ SearchAndFetchFiles,
+ ManagePages,
+ ManageCookiesAndHeaders,
+ ManageDatabase,
+ EditFileNotes,
+ EditFileRelationships,
+ EditFileRatings,
+ ManagePopups,
+ EditFileTimes,
+ CommitPending,
+ SeeLocalPaths,
+ Null = 255,
+}
+
+/// Hydrus key information struct
+#[derive(Deserialize, Debug)]
+pub struct KeyInfo {
+ pub name: String,
+ pub permits_everything: bool,
+ pub basic_permissions: Vec<HydrusPermissions>,
+ pub human_permissions: String,
+}
+
+/// Hydrus service type object
+#[derive(PartialEq, Debug, Clone, Serialize_repr, Deserialize_repr)]
+#[repr(u8)]
+pub enum ServiceType {
+ TagRepository = 0,
+ FileRepository,
+ LocalFileDomain,
+ LocalTagDomain = 5,
+ NumericalRating,
+ BoolRating,
+ AllKnownTags = 10,
+ AllKnownFiles,
+ LocalBooru,
+ IPFS,
+ Trash,
+ AllLocalFiles,
+ FileNotes = 17,
+ ClientAPI,
+ DeletedFromAnywhere,
+ LocalUpdates,
+ AllMyFiles,
+ IncDecRating,
+ ServerAdmin = 99,
+ Null = 255,
+}
+
+/// Hydrus service struct
+#[derive(Deserialize, Debug, Clone)]
+pub struct Service {
+ pub name: String,
+ #[serde(default)]
+ pub service_key: String,
+ pub r#type: ServiceType,
+ pub type_pretty: String,
+ #[serde(default)]
+ pub star_shape: Option<String>,
+ #[serde(default)]
+ pub min_stars: Option<u8>,
+ #[serde(default)]
+ pub max_stars: Option<u8>,
+}
+
+/// Hydrus file domains
+pub enum FileDomain {
+ FileServiceKey(String),
+ FileServiceKeys(Vec<String>),
+ DeletedFileServiceKey(String),
+ DeletedFileServiceKeys(Vec<String>),
+}
+
+/// Payload for importing a file via providing a local path
+#[derive(Serialize, Debug, Default)]
+pub struct AddFileRequest {
+ pub path: PathBuf,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub delete_after_successful_import: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub file_service_key: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub file_service_keys: Option<Vec<String>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub deleted_file_service_key: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub deleted_file_service_keys: Option<Vec<String>>,
+}
+/// File importing status
+#[derive(PartialEq, Debug, Clone, Serialize_repr, Deserialize_repr)]
+#[repr(u8)]
+pub enum AddFileStatus {
+ SuccessfulImport = 1,
+ AlreadyInDatabase,
+ PreviouslyDeleted,
+ FailedToImport,
+ FileVetoed = 7,
+}
+/// File importing api response
+#[derive(Deserialize)]
+pub struct AddFileResponse {
+ pub status: AddFileStatus,
+ pub hash: String,
+ pub note: String,
+}
+
+/// Hydrus file object
+#[derive(Debug, Clone, Serialize)]
+pub enum HydrusFile {
+ #[serde(rename(serialize = "file_id"))]
+ FileId(String),
+ #[serde(rename(serialize = "file_ids"))]
+ FileIds(Vec<String>),
+ #[serde(rename(serialize = "hash"))]
+ Hash(String),
+ #[serde(rename(serialize = "hashes"))]
+ Hashes(Vec<String>),
+}
+
+impl Default for HydrusFile {
+ fn default() -> Self {
+ Self::FileId(String::from(""))
+ }
+}
+
+/// Payload for various file-related requests
+#[derive(Debug, Default)]
+pub struct FileRequest {
+ pub file: HydrusFile,
+ pub delete_after_successful_import: Option<bool>,
+ pub file_service_key: Option<String>,
+ pub file_service_keys: Option<Vec<String>>,
+ pub deleted_file_service_key: Option<String>,
+ pub deleted_file_service_keys: Option<Vec<String>>,
+ pub reason: Option<String>,
+}
+
+impl Serialize for FileRequest {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ use serde::ser::SerializeMap;
+ let mut map = serializer.serialize_map(Some(1))?;
+
+ match &self.file {
+ HydrusFile::FileId(id) => map.serialize_entry("file_id", &id)?,
+ HydrusFile::FileIds(ids) => map.serialize_entry("file_ids", &ids)?,
+ HydrusFile::Hash(hash) => map.serialize_entry("hash", &hash)?,
+ HydrusFile::Hashes(hashes) => map.serialize_entry("hashes", &hashes)?,
+ }
+
+ if let Some(val) = &self.reason {
+ map.serialize_entry("reason", &val)?;
+ }
+
+ if let Some(val) = &self.file_service_key {
+ map.serialize_entry("file_service_key", &val)?;
+ }
+
+ if let Some(val) = &self.file_service_keys {
+ map.serialize_entry("file_service_keys", &val)?;
+ }
+
+ if let Some(val) = &self.deleted_file_service_key {
+ map.serialize_entry("deleted_file_service_key", &val)?;
+ }
+
+ if let Some(val) = &self.deleted_file_service_keys {
+ map.serialize_entry("deleted_file_service_keys", &val)?;
+ }
+
+ map.end()
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct HashResponse {
+ pub hash: String,
+ #[serde(default)]
+ pub perceptual_hashes: Option<Vec<String>>,
+ #[serde(default)]
+ pub pixel_hash: Option<String>,
+}
+
+#[derive(Debug, Deserialize_repr)]
+#[repr(u8)]
+pub enum UrlStatus {
+ NotInDatabase = 0,
+ AlreadyInDatabase = 2,
+ PreviouslyDeleted,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct UrlFileStatus {
+ pub status: UrlStatus,
+ pub hash: String,
+ pub note: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct FilesUrlResponse {
+ pub normalised_url: String,
+ pub url_file_statuses: Vec<UrlFileStatus>,
+}