Merge pull request #9 from Trivernis/develop

Develop
main
Julius Riegel 3 years ago committed by GitHub
commit 87383cc336
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
.gitignore vendored

@ -1,2 +1,3 @@
/target /target
Cargo.lock Cargo.lock
.env

@ -1,6 +1,6 @@
[package] [package]
name = "hydrus-api" name = "hydrus-api"
version = "0.6.0" version = "0.7.0"
authors = ["trivernis <trivernis@protonmail.com>"] authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018" edition = "2018"
license = "Apache-2.0" license = "Apache-2.0"
@ -11,20 +11,21 @@ repository = "https://github.com/trivernis/hydrus-api-rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
serde = {version = "^1.0", features = ["derive"]} serde = { version = "1.0.136", features = ["derive"] }
reqwest = {version = "0.11.4", features = ["json"]} reqwest = { version = "0.11.9", features = ["json"] }
log = "0.4.14" tracing = "0.1.31"
mime = "0.3.16" mime = "0.3.16"
chrono = "0.4.19" chrono = "0.4.19"
regex = "1.5.4" regex = "1.5.4"
lazy_static = "1.4.0" lazy_static = "1.4.0"
[dev-dependencies] [dev-dependencies]
env_logger = "0.8.4"
maplit = "1.0.2" maplit = "1.0.2"
dotenv = "0.15.0"
tracing-subscriber = "0.3.9"
[dev-dependencies.tokio] [dev-dependencies.tokio]
version = "1.8.0" version = "1.17.0"
features = ["macros", "rt-multi-thread"] features = ["macros", "rt-multi-thread"]
[features] [features]

@ -49,13 +49,13 @@ async fn main() {
.run().await.unwrap(); .run().await.unwrap();
for mut file in files { for mut file in files {
file.add_tags(ServiceName::my_tags(), vec![Tag::from("ark mage")]).await.unwrap(); file.add_tags(ServiceName::my_tags().into(), vec![Tag::from("ark mage")]).await.unwrap();
} }
let url = hydrus.import() let url = hydrus.import()
.url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium")
.page(PageIdentifier::name("My Import Page")) .page(PageIdentifier::name("My Import Page"))
.add_additional_tag(ServiceName::my_tags(), Tag::from("character:megumin")) .add_additional_tag(ServiceName::my_tags().into(), Tag::from("character:megumin"))
.show_page(true) .show_page(true)
.run().await.unwrap(); .run().await.unwrap();
} }
@ -67,6 +67,7 @@ async fn main() {
use hydrus_api::Client; use hydrus_api::Client;
use hydrus_api::paths::adding_tags::{AddTagsRequestBuilder, TagAction}; use hydrus_api::paths::adding_tags::{AddTagsRequestBuilder, TagAction};
use std::env; use std::env;
use hydrus_api::api_core::common::ServiceIdentifier;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -81,9 +82,9 @@ async fn main() {
let request = AddTagsRequestBuilder::default() let request = AddTagsRequestBuilder::default()
.add_hash(hash) .add_hash(hash)
// for each tag the service has to be specified // for each tag the service has to be specified
.add_tags("my tags", vec!["beach".into(), "summer".into()]) .add_tags(ServiceIdentifier::name("my tags"), vec!["beach".into(), "summer".into()])
// with tag actions tags can also be removed. It's especially useful for the PTR // with tag actions tags can also be removed. It's especially useful for the PTR
.add_tag_with_action("my tags", "rain", TagAction::DeleteFromLocalService) .add_tag_with_action(ServiceIdentifier::name("my tags"), "rain", TagAction::DeleteFromLocalService)
.build(); .build();
client.add_tags(request).await.unwrap(); client.add_tags(request).await.unwrap();

@ -1,3 +1,4 @@
use crate::api_core::common::ServiceIdentifier;
use crate::api_core::Endpoint; use crate::api_core::Endpoint;
use std::collections::HashMap; use std::collections::HashMap;
@ -21,7 +22,9 @@ impl Endpoint for CleanTags {
pub struct AddTagsRequest { pub struct AddTagsRequest {
pub hashes: Vec<String>, pub hashes: Vec<String>,
pub service_names_to_tags: HashMap<String, Vec<String>>, pub service_names_to_tags: HashMap<String, Vec<String>>,
pub service_keys_to_tags: HashMap<String, Vec<String>>,
pub service_names_to_actions_to_tags: HashMap<String, HashMap<String, Vec<String>>>, pub service_names_to_actions_to_tags: HashMap<String, HashMap<String, Vec<String>>>,
pub service_keys_to_actions_to_tags: HashMap<String, HashMap<String, Vec<String>>>,
} }
pub struct AddTags; pub struct AddTags;
@ -35,10 +38,13 @@ impl Endpoint for AddTags {
} }
} }
#[derive(Default)]
pub struct AddTagsRequestBuilder { pub struct AddTagsRequestBuilder {
hashes: Vec<String>, hashes: Vec<String>,
service_names_to_tags: HashMap<String, Vec<String>>, service_names_to_tags: HashMap<String, Vec<String>>,
service_keys_to_tags: HashMap<String, Vec<String>>,
service_names_to_actions_to_tags: HashMap<String, HashMap<String, Vec<String>>>, service_names_to_actions_to_tags: HashMap<String, HashMap<String, Vec<String>>>,
service_keys_to_actions_to_tags: HashMap<String, HashMap<String, Vec<String>>>,
} }
/// List of actions for a given tag /// List of actions for a given tag
@ -78,16 +84,6 @@ impl TagAction {
} }
} }
impl Default for AddTagsRequestBuilder {
fn default() -> Self {
Self {
hashes: vec![],
service_names_to_tags: Default::default(),
service_names_to_actions_to_tags: Default::default(),
}
}
}
impl AddTagsRequestBuilder { impl AddTagsRequestBuilder {
/// Adds a file hash to the request /// Adds a file hash to the request
pub fn add_hash<S: AsRef<str>>(mut self, hash: S) -> Self { pub fn add_hash<S: AsRef<str>>(mut self, hash: S) -> Self {
@ -104,41 +100,48 @@ impl AddTagsRequestBuilder {
} }
/// Adds a single tag for a given service /// Adds a single tag for a given service
pub fn add_tag<S1: AsRef<str>, S2: AsRef<str>>(mut self, service_name: S1, tag: S2) -> Self { pub fn add_tag<S: AsRef<str>>(mut self, service_id: ServiceIdentifier, tag: S) -> Self {
if let Some(mappings) = self.service_names_to_tags.get_mut(service_name.as_ref()) { let (service, relevant_mappings) = match service_id {
ServiceIdentifier::Name(name) => (name, &mut self.service_names_to_tags),
ServiceIdentifier::Key(key) => (key, &mut self.service_keys_to_tags),
};
if let Some(mappings) = relevant_mappings.get_mut(&service) {
mappings.push(tag.as_ref().into()) mappings.push(tag.as_ref().into())
} else { } else {
self.service_names_to_tags relevant_mappings.insert(service, vec![tag.as_ref().into()]);
.insert(service_name.as_ref().into(), vec![tag.as_ref().into()]);
} }
self self
} }
/// Adds multiple tags for a given service /// Adds multiple tags for a given service
pub fn add_tags<S1: AsRef<str>>(mut self, service_name: S1, mut tags: Vec<String>) -> Self { pub fn add_tags(mut self, service_id: ServiceIdentifier, mut tags: Vec<String>) -> Self {
if let Some(mappings) = self.service_names_to_tags.get_mut(service_name.as_ref()) { let (service, relevant_mappings) = match service_id {
ServiceIdentifier::Name(name) => (name, &mut self.service_names_to_tags),
ServiceIdentifier::Key(key) => (key, &mut self.service_keys_to_tags),
};
if let Some(mappings) = relevant_mappings.get_mut(&service) {
mappings.append(&mut tags); mappings.append(&mut tags);
} else { } else {
self.service_names_to_tags relevant_mappings.insert(service, tags);
.insert(service_name.as_ref().into(), tags);
} }
self self
} }
/// Adds one tag for a given service with a defined action /// Adds one tag for a given service with a defined action
pub fn add_tag_with_action<S1: AsRef<str>, S2: AsRef<str>>( pub fn add_tag_with_action<S: AsRef<str>>(
mut self, mut self,
service_name: S1, service_id: ServiceIdentifier,
tag: S2, tag: S,
action: TagAction, action: TagAction,
) -> Self { ) -> Self {
let (service, relevant_mappings) = match service_id {
ServiceIdentifier::Name(name) => (name, &mut self.service_names_to_actions_to_tags),
ServiceIdentifier::Key(key) => (key, &mut self.service_keys_to_actions_to_tags),
};
let action_id = action.into_id(); let action_id = action.into_id();
if let Some(actions) = self if let Some(actions) = relevant_mappings.get_mut(&service) {
.service_names_to_actions_to_tags
.get_mut(service_name.as_ref())
{
if let Some(tags) = actions.get_mut(&action_id.to_string()) { if let Some(tags) = actions.get_mut(&action_id.to_string()) {
tags.push(tag.as_ref().into()); tags.push(tag.as_ref().into());
} else { } else {
@ -147,8 +150,7 @@ impl AddTagsRequestBuilder {
} else { } else {
let mut actions = HashMap::new(); let mut actions = HashMap::new();
actions.insert(action_id.to_string(), vec![tag.as_ref().into()]); actions.insert(action_id.to_string(), vec![tag.as_ref().into()]);
self.service_names_to_actions_to_tags relevant_mappings.insert(service, actions);
.insert(service_name.as_ref().into(), actions);
} }
self self
} }
@ -158,7 +160,9 @@ impl AddTagsRequestBuilder {
AddTagsRequest { AddTagsRequest {
hashes: self.hashes, hashes: self.hashes,
service_names_to_tags: self.service_names_to_tags, service_names_to_tags: self.service_names_to_tags,
service_keys_to_tags: self.service_keys_to_tags,
service_names_to_actions_to_tags: self.service_names_to_actions_to_tags, service_names_to_actions_to_tags: self.service_names_to_actions_to_tags,
service_keys_to_actions_to_tags: self.service_keys_to_actions_to_tags,
} }
} }
} }

@ -1,3 +1,4 @@
use crate::api_core::common::ServiceIdentifier;
use crate::api_core::Endpoint; use crate::api_core::Endpoint;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
@ -52,7 +53,7 @@ impl Endpoint for GetUrlInfo {
} }
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Default, Debug, Serialize)]
pub struct AddUrlRequest { pub struct AddUrlRequest {
pub url: String, pub url: String,
@ -64,6 +65,7 @@ pub struct AddUrlRequest {
pub show_destination_page: bool, pub show_destination_page: bool,
pub service_names_to_additional_tags: HashMap<String, Vec<String>>, pub service_names_to_additional_tags: HashMap<String, Vec<String>>,
pub service_keys_to_additional_tags: HashMap<String, Vec<String>>,
pub filterable_tags: Vec<String>, pub filterable_tags: Vec<String>,
} }
@ -73,33 +75,20 @@ pub struct AddUrlRequest {
/// Example: /// Example:
/// ``` /// ```
/// use hydrus_api::api_core::adding_urls::AddUrlRequestBuilder; /// use hydrus_api::api_core::adding_urls::AddUrlRequestBuilder;
/// use hydrus_api::api_core::common::ServiceIdentifier;
/// ///
/// let request = AddUrlRequestBuilder::default() /// let request = AddUrlRequestBuilder::default()
/// .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") /// .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium")
/// .add_tags("my tags", vec!["ark mage".to_string(), "grinning".to_string()]) /// .add_tags(ServiceIdentifier::name("my tags"), vec!["ark mage".to_string(), "grinning".to_string()])
/// .show_destination_page(true) /// .show_destination_page(true)
/// .destination_page_name("Rusty Url Import") /// .destination_page_name("Rusty Url Import")
/// .build(); /// .build();
/// ``` /// ```
#[derive(Default)]
pub struct AddUrlRequestBuilder { pub struct AddUrlRequestBuilder {
inner: AddUrlRequest, inner: AddUrlRequest,
} }
impl Default for AddUrlRequestBuilder {
fn default() -> Self {
Self {
inner: AddUrlRequest {
url: String::new(),
destination_page_key: None,
destination_page_name: None,
show_destination_page: false,
service_names_to_additional_tags: Default::default(),
filterable_tags: vec![],
},
}
}
}
impl AddUrlRequestBuilder { impl AddUrlRequestBuilder {
pub fn url<S: ToString>(mut self, url: S) -> Self { pub fn url<S: ToString>(mut self, url: S) -> Self {
self.inner.url = url.to_string(); self.inner.url = url.to_string();
@ -125,17 +114,17 @@ impl AddUrlRequestBuilder {
self self
} }
pub fn add_tags<S: AsRef<str>>(mut self, service: S, mut tags: Vec<String>) -> Self { pub fn add_tags(mut self, service_id: ServiceIdentifier, mut tags: Vec<String>) -> Self {
if let Some(entry) = self let (service, mappings) = match service_id {
.inner ServiceIdentifier::Name(name) => {
.service_names_to_additional_tags (name, &mut self.inner.service_names_to_additional_tags)
.get_mut(service.as_ref()) }
{ ServiceIdentifier::Key(key) => (key, &mut self.inner.service_keys_to_additional_tags),
};
if let Some(entry) = mappings.get_mut(&service) {
entry.append(&mut tags); entry.append(&mut tags);
} else { } else {
self.inner mappings.insert(service, tags);
.service_names_to_additional_tags
.insert(service.as_ref().to_string(), tags);
} }
self self

@ -11,6 +11,7 @@ use crate::api_core::adding_urls::{
AddUrl, AddUrlRequest, AddUrlResponse, AssociateUrl, AssociateUrlRequest, GetUrlFiles, AddUrl, AddUrlRequest, AddUrlResponse, AssociateUrl, AssociateUrlRequest, GetUrlFiles,
GetUrlFilesResponse, GetUrlInfo, GetUrlInfoResponse, GetUrlFilesResponse, GetUrlInfo, GetUrlInfoResponse,
}; };
use crate::api_core::client_builder::ClientBuilder;
use crate::api_core::common::{FileIdentifier, FileMetadataInfo, FileRecord, OptionalStringNumber}; use crate::api_core::common::{FileIdentifier, FileMetadataInfo, FileRecord, OptionalStringNumber};
use crate::api_core::managing_cookies_and_http_headers::{ use crate::api_core::managing_cookies_and_http_headers::{
GetCookies, GetCookiesResponse, SetCookies, SetCookiesRequest, SetUserAgent, GetCookies, GetCookiesResponse, SetCookies, SetCookiesRequest, SetUserAgent,
@ -21,8 +22,8 @@ use crate::api_core::managing_pages::{
GetPages, GetPagesResponse, GetPages, GetPagesResponse,
}; };
use crate::api_core::searching_and_fetching_files::{ use crate::api_core::searching_and_fetching_files::{
FileMetadata, FileMetadataResponse, FileSearchOptions, GetFile, SearchFiles, FileMetadata, FileMetadataResponse, FileSearchOptions, GetFile, SearchFileHashes,
SearchFilesResponse, SearchQueryEntry, SearchFileHashesResponse, SearchFiles, SearchFilesResponse, SearchQueryEntry,
}; };
use crate::api_core::Endpoint; use crate::api_core::Endpoint;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
@ -32,6 +33,7 @@ use crate::utils::{
use reqwest::Response; use reqwest::Response;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use std::fmt::Debug;
static ACCESS_KEY_HEADER: &str = "Hydrus-Client-API-Access-Key"; static ACCESS_KEY_HEADER: &str = "Hydrus-Client-API-Access-Key";
@ -40,12 +42,17 @@ static ACCESS_KEY_HEADER: &str = "Hydrus-Client-API-Access-Key";
/// over the REST api. /// over the REST api.
#[derive(Debug)] #[derive(Debug)]
pub struct Client { pub struct Client {
inner: reqwest::Client, pub(crate) inner: reqwest::Client,
base_url: String, pub(crate) base_url: String,
access_key: String, pub(crate) access_key: String,
} }
impl Client { impl Client {
/// Returns a builder for the client
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
/// Creates a new client to start requests against the hydrus api. /// Creates a new client to start requests against the hydrus api.
pub fn new<S: AsRef<str>>(url: S, access_key: S) -> Self { pub fn new<S: AsRef<str>>(url: S, access_key: S) -> Self {
Self { Self {
@ -56,8 +63,9 @@ impl Client {
} }
/// Starts a get request to the path /// Starts a get request to the path
async fn get<E: Endpoint, Q: Serialize + ?Sized>(&self, query: &Q) -> Result<Response> { #[tracing::instrument(skip(self), level = "trace")]
log::debug!("GET request to {}", E::path()); async fn get<E: Endpoint, Q: Serialize + Debug + ?Sized>(&self, query: &Q) -> Result<Response> {
tracing::trace!("GET request to {}", E::path());
let response = self let response = self
.inner .inner
.get(format!("{}/{}", self.base_url, E::path())) .get(format!("{}/{}", self.base_url, E::path()))
@ -70,7 +78,8 @@ impl Client {
} }
/// Starts a get request to the path associated with the Endpoint Type /// Starts a get request to the path associated with the Endpoint Type
async fn get_and_parse<E: Endpoint, Q: Serialize + ?Sized>( #[tracing::instrument(skip(self), level = "trace")]
async fn get_and_parse<E: Endpoint, Q: Serialize + Debug + ?Sized>(
&self, &self,
query: &Q, query: &Q,
) -> Result<E::Response> { ) -> Result<E::Response> {
@ -80,8 +89,9 @@ impl Client {
} }
/// Stats a post request to the path associated with the Endpoint Type /// Stats a post request to the path associated with the Endpoint Type
#[tracing::instrument(skip(self), level = "trace")]
async fn post<E: Endpoint>(&self, body: E::Request) -> Result<Response> { async fn post<E: Endpoint>(&self, body: E::Request) -> Result<Response> {
log::debug!("POST request to {}", E::path()); tracing::trace!("POST request to {}", E::path());
let response = self let response = self
.inner .inner
.post(format!("{}/{}", self.base_url, E::path())) .post(format!("{}/{}", self.base_url, E::path()))
@ -94,6 +104,7 @@ impl Client {
} }
/// Stats a post request and parses the body as json /// Stats a post request and parses the body as json
#[tracing::instrument(skip(self), level = "trace")]
async fn post_and_parse<E: Endpoint>(&self, body: E::Request) -> Result<E::Response> { async fn post_and_parse<E: Endpoint>(&self, body: E::Request) -> Result<E::Response> {
let response = self.post::<E>(body).await?; let response = self.post::<E>(body).await?;
@ -101,8 +112,9 @@ impl Client {
} }
/// Stats a post request to the path associated with the return type /// Stats a post request to the path associated with the return type
#[tracing::instrument(skip(self, data), level = "trace")]
async fn post_binary<E: Endpoint>(&self, data: Vec<u8>) -> Result<E::Response> { async fn post_binary<E: Endpoint>(&self, data: Vec<u8>) -> Result<E::Response> {
log::debug!("Binary POST request to {}", E::path()); tracing::trace!("Binary POST request to {}", E::path());
let response = self let response = self
.inner .inner
.post(format!("{}/{}", self.base_url, E::path())) .post(format!("{}/{}", self.base_url, E::path()))
@ -117,10 +129,11 @@ impl Client {
} }
/// Returns an error with the response text content if the status doesn't indicate success /// Returns an error with the response text content if the status doesn't indicate success
#[tracing::instrument(level = "trace")]
async fn extract_error(response: Response) -> Result<Response> { async fn extract_error(response: Response) -> Result<Response> {
if !response.status().is_success() { if !response.status().is_success() {
let msg = response.text().await?; let msg = response.text().await?;
log::error!("API returned error '{}'", msg); tracing::error!("API returned error '{}'", msg);
Err(Error::Hydrus(msg)) Err(Error::Hydrus(msg))
} else { } else {
Ok(response) Ok(response)
@ -128,51 +141,55 @@ impl Client {
} }
/// Parses the response as JSOn /// Parses the response as JSOn
async fn extract_content<T: DeserializeOwned>(response: Response) -> Result<T> { #[tracing::instrument(level = "trace")]
response.json::<T>().await.map_err(Error::from) async fn extract_content<T: DeserializeOwned + Debug>(response: Response) -> Result<T> {
let content = response.json::<T>().await?;
tracing::trace!("response content: {:?}", content);
Ok(content)
} }
/// Returns the current API version. It's being incremented every time the API changes. /// Returns the current API version. It's being incremented every time the API changes.
#[tracing::instrument(skip(self), level = "debug")]
pub async fn api_version(&self) -> Result<ApiVersionResponse> { pub async fn api_version(&self) -> Result<ApiVersionResponse> {
log::trace!("Getting api version");
self.get_and_parse::<ApiVersion, ()>(&()).await self.get_and_parse::<ApiVersion, ()>(&()).await
} }
/// Creates a new session key /// Creates a new session key
#[tracing::instrument(skip(self), level = "debug")]
pub async fn session_key(&self) -> Result<SessionKeyResponse> { pub async fn session_key(&self) -> Result<SessionKeyResponse> {
log::trace!("Getting session key");
self.get_and_parse::<SessionKey, ()>(&()).await self.get_and_parse::<SessionKey, ()>(&()).await
} }
/// Verifies if the access key is valid and returns some information about its permissions /// Verifies if the access key is valid and returns some information about its permissions
#[tracing::instrument(skip(self), level = "debug")]
pub async fn verify_access_key(&self) -> Result<VerifyAccessKeyResponse> { pub async fn verify_access_key(&self) -> Result<VerifyAccessKeyResponse> {
log::trace!("Verifying access key");
self.get_and_parse::<VerifyAccessKey, ()>(&()).await self.get_and_parse::<VerifyAccessKey, ()>(&()).await
} }
/// Returns the list of tag and file services of the client /// Returns the list of tag and file services of the client
#[tracing::instrument(skip(self), level = "debug")]
pub async fn get_services(&self) -> Result<GetServicesResponse> { pub async fn get_services(&self) -> Result<GetServicesResponse> {
log::trace!("Getting services");
self.get_and_parse::<GetServices, ()>(&()).await self.get_and_parse::<GetServices, ()>(&()).await
} }
/// Adds a file to hydrus /// Adds a file to hydrus
pub async fn add_file<S: ToString>(&self, path: S) -> Result<AddFileResponse> { #[tracing::instrument(skip(self), level = "debug")]
pub async fn add_file<S: ToString + Debug>(&self, path: S) -> Result<AddFileResponse> {
let path = path.to_string(); let path = path.to_string();
log::trace!("Adding file {}", path);
self.post_and_parse::<AddFile>(AddFileRequest { path }) self.post_and_parse::<AddFile>(AddFileRequest { path })
.await .await
} }
/// Adds a file from binary data to hydrus /// Adds a file from binary data to hydrus
#[tracing::instrument(skip(self, data), level = "debug")]
pub async fn add_binary_file(&self, data: Vec<u8>) -> Result<AddFileResponse> { pub async fn add_binary_file(&self, data: Vec<u8>) -> Result<AddFileResponse> {
log::trace!("Adding binary file");
self.post_binary::<AddFile>(data).await self.post_binary::<AddFile>(data).await
} }
/// Moves files with matching hashes to the trash /// Moves files with matching hashes to the trash
#[tracing::instrument(skip(self), level = "debug")]
pub async fn delete_files(&self, hashes: Vec<String>) -> Result<()> { pub async fn delete_files(&self, hashes: Vec<String>) -> Result<()> {
log::trace!("Deleting files {:?}", hashes);
self.post::<DeleteFiles>(DeleteFilesRequest { hashes }) self.post::<DeleteFiles>(DeleteFilesRequest { hashes })
.await?; .await?;
@ -180,8 +197,8 @@ impl Client {
} }
/// Pulls files out of the trash by hash /// Pulls files out of the trash by hash
#[tracing::instrument(skip(self), level = "debug")]
pub async fn undelete_files(&self, hashes: Vec<String>) -> Result<()> { pub async fn undelete_files(&self, hashes: Vec<String>) -> Result<()> {
log::trace!("Undeleting files {:?}", hashes);
self.post::<UndeleteFiles>(UndeleteFilesRequest { hashes }) self.post::<UndeleteFiles>(UndeleteFilesRequest { hashes })
.await?; .await?;
@ -189,8 +206,8 @@ impl Client {
} }
/// Moves files from the inbox into the archive /// Moves files from the inbox into the archive
#[tracing::instrument(skip(self), level = "debug")]
pub async fn archive_files(&self, hashes: Vec<String>) -> Result<()> { pub async fn archive_files(&self, hashes: Vec<String>) -> Result<()> {
log::trace!("Archiving files {:?}", hashes);
self.post::<ArchiveFiles>(ArchiveFilesRequest { hashes }) self.post::<ArchiveFiles>(ArchiveFilesRequest { hashes })
.await?; .await?;
@ -198,8 +215,8 @@ impl Client {
} }
/// Moves files from the archive into the inbox /// Moves files from the archive into the inbox
#[tracing::instrument(skip(self), level = "debug")]
pub async fn unarchive_files(&self, hashes: Vec<String>) -> Result<()> { pub async fn unarchive_files(&self, hashes: Vec<String>) -> Result<()> {
log::trace!("Unarchiving files {:?}", hashes);
self.post::<UnarchiveFiles>(UnarchiveFilesRequest { hashes }) self.post::<UnarchiveFiles>(UnarchiveFilesRequest { hashes })
.await?; .await?;
@ -207,8 +224,8 @@ impl Client {
} }
/// Returns the list of tags as the client would see them in a human friendly order /// Returns the list of tags as the client would see them in a human friendly order
#[tracing::instrument(skip(self), level = "debug")]
pub async fn clean_tags(&self, tags: Vec<String>) -> Result<CleanTagsResponse> { pub async fn clean_tags(&self, tags: Vec<String>) -> Result<CleanTagsResponse> {
log::trace!("Cleaning tags {:?}", tags);
self.get_and_parse::<CleanTags, [(&str, String)]>(&[( self.get_and_parse::<CleanTags, [(&str, String)]>(&[(
"tags", "tags",
string_list_to_json_array(tags), string_list_to_json_array(tags),
@ -217,37 +234,47 @@ impl Client {
} }
/// Adds tags to files with the given hashes /// Adds tags to files with the given hashes
#[tracing::instrument(skip(self), level = "debug")]
pub async fn add_tags(&self, request: AddTagsRequest) -> Result<()> { pub async fn add_tags(&self, request: AddTagsRequest) -> Result<()> {
log::trace!("Adding tags {:?}", request);
self.post::<AddTags>(request).await?; self.post::<AddTags>(request).await?;
Ok(()) Ok(())
} }
/// Searches for files in the inbox, the archive or both /// Searches for files
#[tracing::instrument(skip(self), level = "debug")]
pub async fn search_files( pub async fn search_files(
&self, &self,
query: Vec<SearchQueryEntry>, query: Vec<SearchQueryEntry>,
options: FileSearchOptions, options: FileSearchOptions,
) -> Result<SearchFilesResponse> { ) -> Result<SearchFilesResponse> {
log::trace!("Searching for files with tags {:?}", query);
let mut args = options.into_query_args(); let mut args = options.into_query_args();
args.push(("tags", search_query_list_to_json_array(query))); args.push(("tags", search_query_list_to_json_array(query)));
self.get_and_parse::<SearchFiles, [(&str, String)]>(&args) self.get_and_parse::<SearchFiles, [(&str, String)]>(&args)
.await .await
} }
/// Searches for file hashes
#[tracing::instrument(skip(self), level = "debug")]
pub async fn search_file_hashes(
&self,
query: Vec<SearchQueryEntry>,
options: FileSearchOptions,
) -> Result<SearchFileHashesResponse> {
let mut args = options.into_query_args();
args.push(("tags", search_query_list_to_json_array(query)));
args.push(("return_hashes", String::from("true")));
self.get_and_parse::<SearchFileHashes, [(&str, String)]>(&args)
.await
}
/// Returns the metadata for a given list of file_ids or hashes /// Returns the metadata for a given list of file_ids or hashes
#[tracing::instrument(skip(self), level = "debug")]
pub async fn get_file_metadata( pub async fn get_file_metadata(
&self, &self,
file_ids: Vec<u64>, file_ids: Vec<u64>,
hashes: Vec<String>, hashes: Vec<String>,
) -> Result<FileMetadataResponse> { ) -> Result<FileMetadataResponse> {
log::trace!(
"Getting file info for ids {:?} or hashes {:?}",
file_ids,
hashes
);
let query = if file_ids.len() > 0 { let query = if file_ids.len() > 0 {
("file_ids", number_list_to_json_array(file_ids)) ("file_ids", number_list_to_json_array(file_ids))
} else { } else {
@ -258,11 +285,11 @@ impl Client {
} }
/// Returns the metadata for a single file identifier /// Returns the metadata for a single file identifier
#[tracing::instrument(skip(self), level = "debug")]
pub async fn get_file_metadata_by_identifier( pub async fn get_file_metadata_by_identifier(
&self, &self,
id: FileIdentifier, id: FileIdentifier,
) -> Result<FileMetadataInfo> { ) -> Result<FileMetadataInfo> {
log::trace!("Getting file metadata {:?}", id);
let mut response = match id.clone() { let mut response = match id.clone() {
FileIdentifier::ID(id) => self.get_file_metadata(vec![id], vec![]).await?, FileIdentifier::ID(id) => self.get_file_metadata(vec![id], vec![]).await?,
FileIdentifier::Hash(hash) => self.get_file_metadata(vec![], vec![hash]).await?, FileIdentifier::Hash(hash) => self.get_file_metadata(vec![], vec![hash]).await?,
@ -275,8 +302,8 @@ impl Client {
} }
/// Returns the bytes of a file from hydrus /// Returns the bytes of a file from hydrus
#[tracing::instrument(skip(self), level = "debug")]
pub async fn get_file(&self, id: FileIdentifier) -> Result<FileRecord> { pub async fn get_file(&self, id: FileIdentifier) -> Result<FileRecord> {
log::trace!("Getting file {:?}", id);
let response = match id { let response = match id {
FileIdentifier::ID(id) => { FileIdentifier::ID(id) => {
self.get::<GetFile, [(&str, u64)]>(&[("file_id", id)]) self.get::<GetFile, [(&str, u64)]>(&[("file_id", id)])
@ -300,28 +327,31 @@ impl Client {
} }
/// Returns all files associated with the given url /// Returns all files associated with the given url
pub async fn get_url_files<S: AsRef<str>>(&self, url: S) -> Result<GetUrlFilesResponse> { #[tracing::instrument(skip(self), level = "debug")]
log::trace!("Getting files for url {}", url.as_ref()); pub async fn get_url_files<S: AsRef<str> + Debug>(
&self,
url: S,
) -> Result<GetUrlFilesResponse> {
self.get_and_parse::<GetUrlFiles, [(&str, &str)]>(&[("url", url.as_ref())]) self.get_and_parse::<GetUrlFiles, [(&str, &str)]>(&[("url", url.as_ref())])
.await .await
} }
/// Returns information about the given url /// Returns information about the given url
pub async fn get_url_info<S: AsRef<str>>(&self, url: S) -> Result<GetUrlInfoResponse> { #[tracing::instrument(skip(self), level = "debug")]
log::trace!("Getting info for url {}", url.as_ref()); pub async fn get_url_info<S: AsRef<str> + Debug>(&self, url: S) -> Result<GetUrlInfoResponse> {
self.get_and_parse::<GetUrlInfo, [(&str, &str)]>(&[("url", url.as_ref())]) self.get_and_parse::<GetUrlInfo, [(&str, &str)]>(&[("url", url.as_ref())])
.await .await
} }
/// Adds an url to hydrus, optionally with additional tags and a destination page /// Adds an url to hydrus, optionally with additional tags and a destination page
#[tracing::instrument(skip(self), level = "debug")]
pub async fn add_url(&self, request: AddUrlRequest) -> Result<AddUrlResponse> { pub async fn add_url(&self, request: AddUrlRequest) -> Result<AddUrlResponse> {
log::trace!("Adding url {:?}", request);
self.post_and_parse::<AddUrl>(request).await self.post_and_parse::<AddUrl>(request).await
} }
/// Associates urls with the given file hashes /// Associates urls with the given file hashes
#[tracing::instrument(skip(self), level = "debug")]
pub async fn associate_urls(&self, urls: Vec<String>, hashes: Vec<String>) -> Result<()> { pub async fn associate_urls(&self, urls: Vec<String>, hashes: Vec<String>) -> Result<()> {
log::trace!("Associating urls {:?} with hashes {:?}", urls, hashes);
self.post::<AssociateUrl>(AssociateUrlRequest { self.post::<AssociateUrl>(AssociateUrlRequest {
hashes, hashes,
urls_to_add: urls, urls_to_add: urls,
@ -333,8 +363,8 @@ impl Client {
} }
/// Disassociates urls with the given file hashes /// Disassociates urls with the given file hashes
#[tracing::instrument(skip(self), level = "debug")]
pub async fn disassociate_urls(&self, urls: Vec<String>, hashes: Vec<String>) -> Result<()> { pub async fn disassociate_urls(&self, urls: Vec<String>, hashes: Vec<String>) -> Result<()> {
log::trace!("Disassociating urls {:?} with hashes {:?}", urls, hashes);
self.post::<AssociateUrl>(AssociateUrlRequest { self.post::<AssociateUrl>(AssociateUrlRequest {
hashes, hashes,
urls_to_add: vec![], urls_to_add: vec![],
@ -346,22 +376,25 @@ impl Client {
} }
/// Returns all pages of the client /// Returns all pages of the client
#[tracing::instrument(skip(self), level = "debug")]
pub async fn get_pages(&self) -> Result<GetPagesResponse> { pub async fn get_pages(&self) -> Result<GetPagesResponse> {
log::trace!("Getting pages");
self.get_and_parse::<GetPages, ()>(&()).await self.get_and_parse::<GetPages, ()>(&()).await
} }
/// Returns information about a single page /// Returns information about a single page
pub async fn get_page_info<S: AsRef<str>>(&self, page_key: S) -> Result<GetPageInfoResponse> { #[tracing::instrument(skip(self), level = "debug")]
log::trace!("Getting information for page {}", page_key.as_ref()); pub async fn get_page_info<S: AsRef<str> + Debug>(
&self,
page_key: S,
) -> Result<GetPageInfoResponse> {
self.get_and_parse::<GetPageInfo, [(&str, &str)]>(&[("page_key", page_key.as_ref())]) self.get_and_parse::<GetPageInfo, [(&str, &str)]>(&[("page_key", page_key.as_ref())])
.await .await
} }
/// Focuses a page in the client /// Focuses a page in the client
pub async fn focus_page<S: ToString>(&self, page_key: S) -> Result<()> { #[tracing::instrument(skip(self), level = "debug")]
pub async fn focus_page<S: ToString + Debug>(&self, page_key: S) -> Result<()> {
let page_key = page_key.to_string(); let page_key = page_key.to_string();
log::trace!("Focussing page {}", page_key);
self.post::<FocusPage>(FocusPageRequest { page_key }) self.post::<FocusPage>(FocusPageRequest { page_key })
.await?; .await?;
@ -369,19 +402,14 @@ impl Client {
} }
/// Adds files to a page /// Adds files to a page
pub async fn add_files_to_page<S: ToString>( #[tracing::instrument(skip(self), level = "debug")]
pub async fn add_files_to_page<S: ToString + Debug>(
&self, &self,
page_key: S, page_key: S,
file_ids: Vec<u64>, file_ids: Vec<u64>,
hashes: Vec<String>, hashes: Vec<String>,
) -> Result<()> { ) -> Result<()> {
let page_key = page_key.to_string(); let page_key = page_key.to_string();
log::trace!(
"Adding files with ids {:?} or hashes {:?} to page {}",
file_ids,
hashes,
page_key
);
self.post::<AddFiles>(AddFilesRequest { self.post::<AddFiles>(AddFilesRequest {
page_key, page_key,
file_ids, file_ids,
@ -393,8 +421,11 @@ impl Client {
} }
/// Returns all cookies for the given domain /// Returns all cookies for the given domain
pub async fn get_cookies<S: AsRef<str>>(&self, domain: S) -> Result<GetCookiesResponse> { #[tracing::instrument(skip(self), level = "debug")]
log::trace!("Getting cookies"); pub async fn get_cookies<S: AsRef<str> + Debug>(
&self,
domain: S,
) -> Result<GetCookiesResponse> {
self.get_and_parse::<GetCookies, [(&str, &str)]>(&[("domain", domain.as_ref())]) self.get_and_parse::<GetCookies, [(&str, &str)]>(&[("domain", domain.as_ref())])
.await .await
} }
@ -402,8 +433,8 @@ impl Client {
/// Sets some cookies for some websites. /// Sets some cookies for some websites.
/// Each entry needs to be in the format `[<name>, <value>, <domain>, <path>, <expires>]` /// Each entry needs to be in the format `[<name>, <value>, <domain>, <path>, <expires>]`
/// with the types `[String, String, String, String, u64]` /// with the types `[String, String, String, String, u64]`
#[tracing::instrument(skip(self), level = "debug")]
pub async fn set_cookies(&self, cookies: Vec<[OptionalStringNumber; 5]>) -> Result<()> { pub async fn set_cookies(&self, cookies: Vec<[OptionalStringNumber; 5]>) -> Result<()> {
log::trace!("Setting cookies {:?}", cookies);
self.post::<SetCookies>(SetCookiesRequest { cookies }) self.post::<SetCookies>(SetCookiesRequest { cookies })
.await?; .await?;
@ -411,9 +442,9 @@ impl Client {
} }
/// Sets the user agent that is being used for every request hydrus starts /// Sets the user agent that is being used for every request hydrus starts
pub async fn set_user_agent<S: ToString>(&self, user_agent: S) -> Result<()> { #[tracing::instrument(skip(self), level = "debug")]
pub async fn set_user_agent<S: ToString + Debug>(&self, user_agent: S) -> Result<()> {
let user_agent = user_agent.to_string(); let user_agent = user_agent.to_string();
log::trace!("Setting user agent to {}", user_agent);
self.post::<SetUserAgent>(SetUserAgentRequest { user_agent }) self.post::<SetUserAgent>(SetUserAgentRequest { user_agent })
.await?; .await?;

@ -0,0 +1,56 @@
use crate::error::{Error, Result};
use crate::Client;
use std::time::Duration;
pub struct ClientBuilder {
reqwest_builder: reqwest::ClientBuilder,
base_url: String,
access_key: Option<String>,
}
impl Default for ClientBuilder {
fn default() -> Self {
Self {
reqwest_builder: Default::default(),
base_url: "127.0.0.1:45869".to_string(),
access_key: None,
}
}
}
impl ClientBuilder {
/// Set the base url with port for the client api
/// The default value is `127.0.0.1:45869`
pub fn url<S: ToString>(mut self, url: S) -> Self {
self.base_url = url.to_string();
self
}
/// Sets the access key for the client.
/// The key is required
pub fn access_key<S: ToString>(mut self, key: S) -> Self {
self.access_key = Some(key.to_string());
self
}
/// Sets the default timeout for requests to the API
pub fn timeout(mut self, timeout: Duration) -> Self {
self.reqwest_builder = self.reqwest_builder.timeout(timeout);
self
}
/// Builds the client
pub fn build(self) -> Result<Client> {
let access_key = self
.access_key
.ok_or_else(|| Error::BuildError(String::from("missing access key")))?;
Ok(Client {
inner: self.reqwest_builder.build()?,
base_url: self.base_url,
access_key,
})
}
}

@ -6,6 +6,38 @@ pub struct BasicServiceInfo {
pub service_key: String, pub service_key: String,
} }
impl BasicServiceInfo {
/// Converts the Service into into an identifier
/// that can be used for requests consuming service references
pub fn into_id(self) -> ServiceIdentifier {
ServiceIdentifier::Key(self.service_key)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialOrd, PartialEq, Ord, Eq)]
pub enum ServiceIdentifier {
/// Try to avoid using this variant as it will be removed from the interface
/// in the future
Name(String),
/// The key variant of a service which should be the preferred variant.
Key(String),
}
impl ServiceIdentifier {
/// Deprecation: use [ServiceIdentifier::key] instead.
#[deprecated(
note = "Deprecation in the official interface was mentioned. Use the service keys instead."
)]
pub fn name<S: ToString>(name: S) -> Self {
Self::Name(name.to_string())
}
/// Constructs a new type of the key variant.
pub fn key<S: ToString>(key: S) -> Self {
Self::Key(key.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BasicHashList { pub struct BasicHashList {
pub hashes: Vec<String>, pub hashes: Vec<String>,
@ -21,6 +53,8 @@ pub struct FileMetadataInfo {
pub width: Option<u32>, pub width: Option<u32>,
pub height: Option<u32>, pub height: Option<u32>,
pub duration: Option<u64>, pub duration: Option<u64>,
pub time_modified: Option<u64>,
pub file_services: FileMetadataServices,
pub has_audio: Option<bool>, pub has_audio: Option<bool>,
pub num_frames: Option<u64>, pub num_frames: Option<u64>,
pub num_words: Option<u64>, pub num_words: Option<u64>,
@ -28,8 +62,12 @@ pub struct FileMetadataInfo {
pub is_local: bool, pub is_local: bool,
pub is_trashed: bool, pub is_trashed: bool,
pub known_urls: Vec<String>, pub known_urls: Vec<String>,
#[deprecated]
pub service_names_to_statuses_to_tags: HashMap<String, HashMap<String, Vec<String>>>, pub service_names_to_statuses_to_tags: HashMap<String, HashMap<String, Vec<String>>>,
pub service_keys_to_statuses_to_tags: HashMap<String, HashMap<String, Vec<String>>>,
#[deprecated]
pub service_names_to_statuses_to_display_tags: HashMap<String, HashMap<String, Vec<String>>>, pub service_names_to_statuses_to_display_tags: HashMap<String, HashMap<String, Vec<String>>>,
pub service_keys_to_statuses_to_display_tags: HashMap<String, HashMap<String, Vec<String>>>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -50,6 +88,23 @@ pub struct FileRecord {
pub mime_type: String, pub mime_type: String,
} }
#[derive(Clone, Default, Debug, Deserialize)]
pub struct FileMetadataServices {
pub current: HashMap<String, FileMetadataServiceCurrent>,
pub deleted: HashMap<String, FileMetadataServiceDeleted>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct FileMetadataServiceCurrent {
pub time_imported: u64,
}
#[derive(Clone, Debug, Deserialize)]
pub struct FileMetadataServiceDeleted {
pub time_deleted: u64,
pub time_imported: u64,
}
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct PageInformation { pub struct PageInformation {
pub name: String, pub name: String,

@ -1,20 +1,23 @@
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use std::fmt::Debug;
pub mod access_management; pub mod access_management;
pub mod adding_files; pub mod adding_files;
pub mod adding_tags; pub mod adding_tags;
pub mod adding_urls; pub mod adding_urls;
pub mod client; pub mod client;
pub mod client_builder;
pub mod common; pub mod common;
pub mod managing_cookies_and_http_headers; pub mod managing_cookies_and_http_headers;
pub mod managing_pages; pub mod managing_pages;
pub mod searching_and_fetching_files; pub mod searching_and_fetching_files;
pub use searching_and_fetching_files::file_sort_type; pub use searching_and_fetching_files::file_sort_type;
pub(crate) trait Endpoint { pub(crate) trait Endpoint {
type Request: Serialize; type Request: Serialize + Debug;
type Response: DeserializeOwned; type Response: DeserializeOwned + Debug;
fn path() -> String; fn path() -> String;
} }

@ -112,6 +112,22 @@ impl Endpoint for SearchFiles {
} }
} }
#[derive(Clone, Debug, Deserialize)]
pub struct SearchFileHashesResponse {
pub hashes: Vec<String>,
}
pub struct SearchFileHashes;
impl Endpoint for SearchFileHashes {
type Request = ();
type Response = SearchFileHashesResponse;
fn path() -> String {
String::from("get_files/search_files")
}
}
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
pub struct FileMetadataResponse { pub struct FileMetadataResponse {
pub metadata: Vec<FileMetadataInfo>, pub metadata: Vec<FileMetadataInfo>,

@ -13,6 +13,7 @@ pub enum Error {
ImportFailed(String), ImportFailed(String),
FileNotFound(FileIdentifier), FileNotFound(FileIdentifier),
InvalidMime(String), InvalidMime(String),
BuildError(String),
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@ -27,6 +28,7 @@ impl fmt::Display for Error {
Self::ImportVetoed(msg) => write!(f, "File import vetoed: {}", msg), Self::ImportVetoed(msg) => write!(f, "File import vetoed: {}", msg),
Self::FileNotFound(id) => write!(f, "File {:?} not found", id), Self::FileNotFound(id) => write!(f, "File {:?} not found", id),
Self::InvalidMime(mime) => write!(f, "Failed to parse invalid mime {}", mime), Self::InvalidMime(mime) => write!(f, "Failed to parse invalid mime {}", mime),
Self::BuildError(error) => write!(f, "Build error {}", error),
} }
} }
} }

@ -6,7 +6,7 @@
//! ## Hydrus Usage Example //! ## Hydrus Usage Example
//! //!
//! ``` //! ```
//! # use hydrus_api::{Hydrus, Client}; //! use hydrus_api::{Hydrus, Client};
//! use std::env; //! use std::env;
//! use hydrus_api::wrapper::tag::Tag; //! use hydrus_api::wrapper::tag::Tag;
//! use hydrus_api::wrapper::service::ServiceName; //! use hydrus_api::wrapper::service::ServiceName;
@ -24,7 +24,8 @@
//! let files = hydrus.search() //! let files = hydrus.search()
//! .add_tag(Tag::from("character:megumin")) //! .add_tag(Tag::from("character:megumin"))
//! .add_tag(SystemTagBuilder::new().archive().build()) //! .add_tag(SystemTagBuilder::new().archive().build())
//! .add_tag(SystemTagBuilder::new().tag_namespace_as_number("page", Comparator::Equal, 5).negate().build()) //! .add_tag(SystemTagBuilder::new()
//! .tag_namespace_as_number("page", Comparator::Equal, 5).negate().build())
//! .add_or_chain( //! .add_or_chain(
//! OrChainBuilder::new() //! OrChainBuilder::new()
//! .add_tag("summer".into()) //! .add_tag("summer".into())
@ -36,13 +37,13 @@
//! .run().await.unwrap(); //! .run().await.unwrap();
//! //!
//! for mut file in files { //! for mut file in files {
//! file.add_tags(ServiceName::my_tags(), vec![Tag::from("ark mage")]).await.unwrap(); //! file.add_tags(ServiceName::my_tags().into(), vec![Tag::from("ark mage")]).await.unwrap();
//! } //! }
//! //!
//! let url = hydrus.import() //! let url = hydrus.import()
//! .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") //! .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium")
//! .page(PageIdentifier::name("My Import Page")) //! .page(PageIdentifier::name("My Import Page"))
//! .add_additional_tag(ServiceName::my_tags(), Tag::from("character:megumin")) //! .add_additional_tag(ServiceName::my_tags().into(), Tag::from("character:megumin"))
//! .show_page(true) //! .show_page(true)
//! .run().await.unwrap(); //! .run().await.unwrap();
//! # } //! # }
@ -53,6 +54,7 @@
//! use hydrus_api::Client; //! use hydrus_api::Client;
//! use hydrus_api::api_core::adding_tags::{AddTagsRequestBuilder, TagAction}; //! use hydrus_api::api_core::adding_tags::{AddTagsRequestBuilder, TagAction};
//! use std::env; //! use std::env;
//! use hydrus_api::api_core::common::ServiceIdentifier;
//! # #[tokio::test] //! # #[tokio::test]
//! # async fn doctest() { //! # async fn doctest() {
//! //!
@ -67,9 +69,9 @@
//! let request = AddTagsRequestBuilder::default() //! let request = AddTagsRequestBuilder::default()
//! .add_hash(hash) //! .add_hash(hash)
//! // for each tag the service has to be specified //! // for each tag the service has to be specified
//! .add_tags("my tags", vec!["beach".into(), "summer".into()]) //! .add_tags(ServiceIdentifier::name("my tags"), vec!["beach".into(), "summer".into()])
//! // with tag actions tags can also be removed. It's especially useful for the PTR //! // with tag actions tags can also be removed. It's especially useful for the PTR
//! .add_tag_with_action("my tags", "rain", TagAction::DeleteFromLocalService) //! .add_tag_with_action(ServiceIdentifier::name("my tags"), "rain", TagAction::DeleteFromLocalService)
//! .build(); //! .build();
//! //!
//! client.add_tags(request).await.unwrap(); //! client.add_tags(request).await.unwrap();

@ -1,10 +1,10 @@
use crate::api_core::adding_files::{STATUS_IMPORT_FAILED, STATUS_IMPORT_VETOED}; use crate::api_core::adding_files::{STATUS_IMPORT_FAILED, STATUS_IMPORT_VETOED};
use crate::api_core::adding_urls::AddUrlRequestBuilder; use crate::api_core::adding_urls::AddUrlRequestBuilder;
use crate::api_core::common::ServiceIdentifier;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::utils::tag_list_to_string_list; use crate::utils::tag_list_to_string_list;
use crate::wrapper::hydrus_file::HydrusFile; use crate::wrapper::hydrus_file::HydrusFile;
use crate::wrapper::page::PageIdentifier; use crate::wrapper::page::PageIdentifier;
use crate::wrapper::service::ServiceName;
use crate::wrapper::tag::Tag; use crate::wrapper::tag::Tag;
use crate::wrapper::url::Url; use crate::wrapper::url::Url;
use crate::Client; use crate::Client;
@ -77,7 +77,7 @@ pub struct UrlImportBuilder {
page: Option<PageIdentifier>, page: Option<PageIdentifier>,
show_page: bool, show_page: bool,
filter_tags: Vec<Tag>, filter_tags: Vec<Tag>,
service_tag_mappings: HashMap<String, Vec<Tag>>, service_tag_mappings: HashMap<ServiceIdentifier, Vec<Tag>>,
} }
impl UrlImportBuilder { impl UrlImportBuilder {
@ -121,16 +121,16 @@ impl UrlImportBuilder {
} }
/// Adds an additional tag for the imported file /// Adds an additional tag for the imported file
pub fn add_additional_tag(self, service: ServiceName, tag: Tag) -> Self { pub fn add_additional_tag(self, service: ServiceIdentifier, tag: Tag) -> Self {
self.add_additional_tags(service, vec![tag]) self.add_additional_tags(service, vec![tag])
} }
/// Adds multiple additional tags for the import /// Adds multiple additional tags for the import
pub fn add_additional_tags(mut self, service: ServiceName, mut tags: Vec<Tag>) -> Self { pub fn add_additional_tags(mut self, service: ServiceIdentifier, mut tags: Vec<Tag>) -> Self {
if let Some(service_tags) = self.service_tag_mappings.get_mut(&service.0) { if let Some(service_tags) = self.service_tag_mappings.get_mut(&service) {
service_tags.append(&mut tags); service_tags.append(&mut tags);
} else { } else {
self.service_tag_mappings.insert(service.0, tags); self.service_tag_mappings.insert(service, tags);
} }
self self

@ -1,6 +1,6 @@
use crate::api_core::adding_tags::{AddTagsRequestBuilder, TagAction}; use crate::api_core::adding_tags::{AddTagsRequestBuilder, TagAction};
use crate::api_core::common::ServiceIdentifier;
use crate::error::Result; use crate::error::Result;
use crate::wrapper::service::ServiceName;
use crate::wrapper::tag::Tag; use crate::wrapper::tag::Tag;
use crate::Client; use crate::Client;
use std::collections::HashMap; use std::collections::HashMap;
@ -8,7 +8,7 @@ use std::collections::HashMap;
pub struct TaggingBuilder { pub struct TaggingBuilder {
client: Client, client: Client,
hashes: Vec<String>, hashes: Vec<String>,
tag_mappings: HashMap<ServiceName, HashMap<TagAction, Vec<Tag>>>, tag_mappings: HashMap<ServiceIdentifier, HashMap<TagAction, Vec<Tag>>>,
} }
impl TaggingBuilder { impl TaggingBuilder {
@ -28,12 +28,17 @@ impl TaggingBuilder {
} }
/// Adds a single tag for a given service /// Adds a single tag for a given service
pub fn add_tag(self, service: ServiceName, action: TagAction, tag: Tag) -> Self { pub fn add_tag(self, service: ServiceIdentifier, action: TagAction, tag: Tag) -> Self {
self.add_tags(service, action, vec![tag]) self.add_tags(service, action, vec![tag])
} }
/// Adds tags with actions for the given service /// Adds tags with actions for the given service
pub fn add_tags(mut self, service: ServiceName, action: TagAction, mut tags: Vec<Tag>) -> Self { pub fn add_tags(
mut self,
service: ServiceIdentifier,
action: TagAction,
mut tags: Vec<Tag>,
) -> Self {
let service_action_mappings = let service_action_mappings =
if let Some(service_action_mappings) = self.tag_mappings.get_mut(&service) { if let Some(service_action_mappings) = self.tag_mappings.get_mut(&service) {
service_action_mappings service_action_mappings
@ -57,7 +62,7 @@ impl TaggingBuilder {
for (action, tags) in action_tag_mappings { for (action, tags) in action_tag_mappings {
for tag in tags { for tag in tags {
request = request.add_tag_with_action( request = request.add_tag_with_action(
service.0.clone(), service.clone().into(),
tag.to_string(), tag.to_string(),
action.clone(), action.clone(),
); );

@ -10,6 +10,7 @@ use crate::wrapper::service::Services;
use crate::wrapper::url::Url; use crate::wrapper::url::Url;
use crate::wrapper::version::Version; use crate::wrapper::version::Version;
use crate::Client; use crate::Client;
use std::fmt::Debug;
/// A high level wrapper for the hydrus API for easier management of files, tags /// A high level wrapper for the hydrus API for easier management of files, tags
/// urls etc. /// urls etc.
@ -47,13 +48,13 @@ impl Hydrus {
} }
/// Returns the address as an object that can be used to get and set cookies /// Returns the address as an object that can be used to get and set cookies
pub fn address<S: AsRef<str>>(&self, address: S) -> Address { pub fn address<S: AsRef<str> + Debug>(&self, address: S) -> Address {
Address::from_str(self.client.clone(), address.as_ref()) Address::from_str(self.client.clone(), address.as_ref())
} }
/// Returns information about a given url in an object that allows /// Returns information about a given url in an object that allows
/// further operations with that url /// further operations with that url
pub async fn url<S: AsRef<str>>(&self, url: S) -> Result<Url> { pub async fn url<S: AsRef<str> + Debug>(&self, url: S) -> Result<Url> {
let info = self.client.get_url_info(&url).await?; let info = self.client.get_url_info(&url).await?;
Ok(Url { Ok(Url {
@ -87,7 +88,7 @@ impl Hydrus {
} }
/// Returns a hydrus page by page key /// Returns a hydrus page by page key
pub async fn page<S: AsRef<str>>(&self, page_key: S) -> Result<HydrusPage> { pub async fn page<S: AsRef<str> + Debug>(&self, page_key: S) -> Result<HydrusPage> {
let info_response = self.client.get_page_info(page_key).await?; let info_response = self.client.get_page_info(page_key).await?;
Ok(HydrusPage::from_info( Ok(HydrusPage::from_info(
@ -107,7 +108,7 @@ impl Hydrus {
} }
/// Sets the user agent hydrus uses for http requests /// Sets the user agent hydrus uses for http requests
pub async fn set_user_agent<S: ToString>(&self, user_agent: S) -> Result<()> { pub async fn set_user_agent<S: ToString + Debug>(&self, user_agent: S) -> Result<()> {
self.client.set_user_agent(user_agent).await self.client.set_user_agent(user_agent).await
} }
} }

@ -1,10 +1,11 @@
use crate::api_core::adding_tags::{AddTagsRequestBuilder, TagAction}; use crate::api_core::adding_tags::{AddTagsRequestBuilder, TagAction};
use crate::api_core::common::{FileIdentifier, FileMetadataInfo, FileRecord}; use crate::api_core::common::{FileIdentifier, FileMetadataInfo, FileRecord, ServiceIdentifier};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::utils::tag_list_to_string_list; use crate::utils::tag_list_to_string_list;
use crate::wrapper::service::ServiceName; use crate::wrapper::service::ServiceName;
use crate::wrapper::tag::Tag; use crate::wrapper::tag::Tag;
use crate::Client; use crate::Client;
use chrono::{NaiveDateTime, TimeZone, Utc};
use mime::Mime; use mime::Mime;
use std::collections::HashMap; use std::collections::HashMap;
@ -171,6 +172,62 @@ impl HydrusFile {
Ok(metadata.is_trashed) 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
.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)
}
/// Associates the file with a list of urls /// Associates the file with a list of urls
pub async fn associate_urls(&mut self, urls: Vec<String>) -> Result<()> { pub async fn associate_urls(&mut self, urls: Vec<String>) -> Result<()> {
let hash = self.hash().await?; let hash = self.hash().await?;
@ -183,11 +240,15 @@ impl HydrusFile {
self.client.disassociate_urls(urls, vec![hash]).await self.client.disassociate_urls(urls, vec![hash]).await
} }
/// Returns map mapping lists of tags to services /// Returns map mapping lists of tags to services.
pub async fn services_with_tags(&mut self) -> Result<HashMap<ServiceName, Vec<Tag>>> { ///
/// 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 metadata = self.metadata().await?;
let mut tag_mappings = HashMap::new(); let mut tag_mappings = HashMap::new();
#[allow(deprecated)]
for (service, status_tags) in &metadata.service_names_to_statuses_to_tags { for (service, status_tags) in &metadata.service_names_to_statuses_to_tags {
let mut tag_list = Vec::new(); let mut tag_list = Vec::new();
@ -200,6 +261,23 @@ impl HydrusFile {
Ok(tag_mappings) 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 /// Returns a list of all tags assigned to the file
pub async fn tags(&mut self) -> Result<Vec<Tag>> { pub async fn tags(&mut self) -> Result<Vec<Tag>> {
let mut tag_list = Vec::new(); let mut tag_list = Vec::new();
@ -213,11 +291,11 @@ impl HydrusFile {
} }
/// Adds tags for a specific service to the file /// Adds tags for a specific service to the file
pub async fn add_tags(&mut self, service: ServiceName, tags: Vec<Tag>) -> Result<()> { pub async fn add_tags(&mut self, service: ServiceIdentifier, tags: Vec<Tag>) -> Result<()> {
let hash = self.hash().await?; let hash = self.hash().await?;
let request = AddTagsRequestBuilder::default() let request = AddTagsRequestBuilder::default()
.add_hash(hash) .add_hash(hash)
.add_tags(service.0, tag_list_to_string_list(tags)) .add_tags(service, tag_list_to_string_list(tags))
.build(); .build();
self.client.add_tags(request).await self.client.add_tags(request).await
@ -226,7 +304,7 @@ impl HydrusFile {
/// Allows modification of tags by using the defined tag actions /// Allows modification of tags by using the defined tag actions
pub async fn modify_tags( pub async fn modify_tags(
&mut self, &mut self,
service: ServiceName, service: ServiceIdentifier,
action: TagAction, action: TagAction,
tags: Vec<Tag>, tags: Vec<Tag>,
) -> Result<()> { ) -> Result<()> {
@ -234,8 +312,7 @@ impl HydrusFile {
let mut reqwest = AddTagsRequestBuilder::default().add_hash(hash); let mut reqwest = AddTagsRequestBuilder::default().add_hash(hash);
for tag in tags { for tag in tags {
reqwest = reqwest = reqwest.add_tag_with_action(service.clone(), tag.to_string(), action.clone());
reqwest.add_tag_with_action(service.0.clone(), tag.to_string(), action.clone());
} }
self.client.add_tags(reqwest.build()).await self.client.add_tags(reqwest.build()).await

@ -47,7 +47,7 @@ where
}) })
.map(Tag::from) .map(Tag::from)
.collect(); .collect();
log::debug!("String parsed to or-chain {:?}", tags); tracing::debug!("String parsed to or-chain {:?}", tags);
Self { tags } Self { tags }
} }

@ -5,6 +5,7 @@ use crate::api_core::access_management::{
SERVICE_TYPE_TAG_REPOSITORIES, SERVICE_TYPE_TRASH, SERVICE_TYPE_TAG_REPOSITORIES, SERVICE_TYPE_TRASH,
}; };
use crate::api_core::common::ServiceIdentifier;
use crate::error::Error; use crate::error::Error;
use crate::wrapper::builders::search_builder::SearchBuilder; use crate::wrapper::builders::search_builder::SearchBuilder;
use crate::Client; use crate::Client;
@ -96,6 +97,12 @@ impl Display for ServiceName {
} }
} }
impl Into<ServiceIdentifier> for ServiceName {
fn into(self) -> ServiceIdentifier {
ServiceIdentifier::Name(self.0)
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct Service { pub struct Service {
client: Client, client: Client,

@ -1,5 +1,6 @@
use super::super::common; use super::super::common;
use hydrus_api::api_core::adding_tags::{AddTagsRequestBuilder, TagAction}; use hydrus_api::api_core::adding_tags::{AddTagsRequestBuilder, TagAction};
use hydrus_api::api_core::common::ServiceIdentifier;
#[tokio::test] #[tokio::test]
async fn it_cleans_tags() { async fn it_cleans_tags() {
@ -18,11 +19,19 @@ async fn it_cleans_tags() {
#[tokio::test] #[tokio::test]
async fn it_adds_tags() { async fn it_adds_tags() {
#![allow(deprecated)]
let client = common::get_client(); let client = common::get_client();
let request = AddTagsRequestBuilder::default() let request = AddTagsRequestBuilder::default()
.add_hash("0000000000000000000000000000000000000000000000000000000000000000") // valid hash, I hope no files are affected .add_hash("0000000000000000000000000000000000000000000000000000000000000000") // valid hash, I hope no files are affected
.add_tags("my tags", vec!["beach".into(), "summer".into()]) .add_tags(
.add_tag_with_action("my tags", "rain", TagAction::DeleteFromLocalService) ServiceIdentifier::name("my tags"),
vec!["beach".into(), "summer".into()],
)
.add_tag_with_action(
ServiceIdentifier::name("my tags"),
"rain",
TagAction::DeleteFromLocalService,
)
.build(); .build();
client.add_tags(request).await.unwrap(); client.add_tags(request).await.unwrap();
} }

@ -1,5 +1,6 @@
use super::super::common; use super::super::common;
use hydrus_api::api_core::adding_urls::{AddUrlRequestBuilder, URL_TYPE_POST}; use hydrus_api::api_core::adding_urls::{AddUrlRequestBuilder, URL_TYPE_POST};
use hydrus_api::api_core::common::ServiceIdentifier;
#[tokio::test] #[tokio::test]
async fn it_returns_files_for_an_url() { async fn it_returns_files_for_an_url() {
@ -25,11 +26,12 @@ async fn it_returns_url_information() {
#[tokio::test] #[tokio::test]
async fn it_adds_urls() { async fn it_adds_urls() {
#![allow(deprecated)]
let client = common::get_client(); let client = common::get_client();
let request = AddUrlRequestBuilder::default() let request = AddUrlRequestBuilder::default()
.url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium")
.add_tags( .add_tags(
"my tags", ServiceIdentifier::name("my tags"),
vec!["ark mage".to_string(), "grinning".to_string()], vec!["ark mage".to_string(), "grinning".to_string()],
) )
.show_destination_page(true) .show_destination_page(true)

@ -8,7 +8,7 @@ async fn is_searches_files() {
let client = common::get_client(); let client = common::get_client();
let options = FileSearchOptions::new() let options = FileSearchOptions::new()
.sort_type(SORT_FILE_PIXEL_COUNT) .sort_type(SORT_FILE_PIXEL_COUNT)
.tag_service_name("public tag repository") .tag_service_name("my tags")
.file_service_name("all known files"); .file_service_name("all known files");
client client
.search_files( .search_files(
@ -22,16 +22,35 @@ async fn is_searches_files() {
.unwrap(); .unwrap();
} }
#[tokio::test]
async fn is_searches_file_hashes() {
let client = common::get_client();
let options = FileSearchOptions::new()
.sort_type(SORT_FILE_PIXEL_COUNT)
.tag_service_name("my tags")
.file_service_name("all known files");
client
.search_file_hashes(
vec![
"beach".into(),
SearchQueryEntry::OrChain(vec!["summer".to_string(), "winter".to_string()]),
],
options,
)
.await
.unwrap();
}
#[tokio::test] #[tokio::test]
async fn it_fetches_file_metadata() { async fn it_fetches_file_metadata() {
let client = common::get_client(); let client = common::get_client();
let response = client client
.get_file_metadata( .get_file_metadata(
vec![], vec![],
vec!["0000000000000000000000000000000000000000000000000000000000000000".to_string()], vec!["0000000000000000000000000000000000000000000000000000000000000000".to_string()],
) )
.await; .await
assert!(response.is_ok()); // Even if the file doesn't exist it still returns some information about it .unwrap();
} }
#[tokio::test] #[tokio::test]

@ -1,26 +1,28 @@
use hydrus_api::api_core::client::Client; use hydrus_api::api_core::client::Client;
use hydrus_api::Hydrus; use hydrus_api::Hydrus;
use log::LevelFilter;
use std::env; use std::env;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::Arc; use std::time::Duration;
pub fn setup() { pub fn setup() {
lazy_static::lazy_static! { static ref SETUP_DONE: Arc<AtomicBool> = Arc::new(AtomicBool::new(false)); } lazy_static::lazy_static! { static ref SETUP_DONE: Arc<Mutex<bool>> = Arc::new(Mutex::new(false)); }
if !SETUP_DONE.swap(true, Ordering::SeqCst) { let mut setup_done: MutexGuard<bool> = SETUP_DONE.lock().unwrap();
env_logger::builder()
.filter_level(LevelFilter::Trace) if !*setup_done {
.init(); dotenv::dotenv().expect("failed to initialize dotenv");
tracing_subscriber::fmt::init();
*setup_done = true;
} }
} }
pub fn get_client() -> Client { pub fn get_client() -> Client {
setup(); setup();
Client::builder()
Client::new( .url(env::var("HYDRUS_URL").unwrap())
env::var("HYDRUS_URL").unwrap(), .access_key(env::var("HYDRUS_ACCESS_KEY").unwrap())
env::var("HYDRUS_ACCESS_KEY").unwrap(), .timeout(Duration::from_secs(5))
) .build()
.unwrap()
} }
pub fn get_hydrus() -> Hydrus { pub fn get_hydrus() -> Hydrus {

@ -54,7 +54,7 @@ async fn it_has_tags() {
async fn it_adds_tags() { async fn it_adds_tags() {
let mut file = get_file().await; let mut file = get_file().await;
file.add_tags( file.add_tags(
ServiceName::public_tag_repository(), ServiceName::my_tags().into(),
vec!["character:megumin".into(), "ark mage".into()], vec!["character:megumin".into(), "ark mage".into()],
) )
.await .await
@ -65,7 +65,7 @@ async fn it_adds_tags() {
async fn it_modifies_tags() { async fn it_modifies_tags() {
let mut file = get_file().await; let mut file = get_file().await;
file.modify_tags( file.modify_tags(
ServiceName::public_tag_repository(), ServiceName::my_tags().into(),
TagAction::RescindPendFromRepository, TagAction::RescindPendFromRepository,
vec!["ark mage".into()], vec!["ark mage".into()],
) )
@ -87,4 +87,7 @@ async fn it_retrieves_metadata() {
assert!(file.dimensions().await.unwrap().is_some()); assert!(file.dimensions().await.unwrap().is_some());
assert!(file.stored_locally().await.unwrap()); assert!(file.stored_locally().await.unwrap());
assert!(file.duration().await.unwrap().is_none()); assert!(file.duration().await.unwrap().is_none());
assert!(file.time_modified().await.is_ok());
assert!(file.time_deleted("000").await.is_ok());
assert!(file.time_imported("000").await.is_ok());
} }

@ -60,7 +60,7 @@ async fn it_adds_tags() {
hydrus hydrus
.tagging() .tagging()
.add_tag( .add_tag(
ServiceName::my_tags(), ServiceName::my_tags().into(),
TagAction::AddToLocalService, TagAction::AddToLocalService,
"summer".into(), "summer".into(),
) )

@ -39,8 +39,11 @@ async fn it_imports_urls() {
.url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium") .url("https://www.pixiv.net/member_illust.php?illust_id=83406361&mode=medium")
.page(PageIdentifier::name("Rusty Import")) .page(PageIdentifier::name("Rusty Import"))
.show_page(true) .show_page(true)
.add_additional_tag(ServiceName::my_tags(), Tag::from("ark mage")) .add_additional_tag(ServiceName::my_tags().into(), Tag::from("ark mage"))
.add_additional_tag(ServiceName::my_tags(), Tag::from("character:megumin")) .add_additional_tag(
ServiceName::my_tags().into(),
Tag::from("character:megumin"),
)
.run() .run()
.await .await
.unwrap(); .unwrap();

@ -7,8 +7,12 @@ use hydrus_api::wrapper::builders::tag_builder::{
}; };
use hydrus_api::wrapper::service::ServiceName; use hydrus_api::wrapper::service::ServiceName;
use hydrus_api::wrapper::tag::Tag; use hydrus_api::wrapper::tag::Tag;
use std::sync::Arc;
use tokio::sync::Mutex;
async fn retrieve_single_tag(tag: Tag) -> Result<()> { async fn retrieve_single_tag(tag: Tag) -> Result<()> {
lazy_static::lazy_static! { static ref SEM: Arc<Mutex<()>> = Arc::new(Mutex::new(())); }
SEM.lock().await;
let hydrus = common::get_hydrus(); let hydrus = common::get_hydrus();
hydrus.search().add_tag(tag).run().await?; hydrus.search().add_tag(tag).run().await?;

@ -18,7 +18,10 @@ async fn it_imports() {
url.import() url.import()
.page(PageIdentifier::name("Rusty Import")) .page(PageIdentifier::name("Rusty Import"))
.add_additional_tag(ServiceName::my_tags(), Tag::from("character:megumin")) .add_additional_tag(
ServiceName::my_tags().into(),
Tag::from("character:megumin"),
)
.run() .run()
.await .await
.unwrap(); .unwrap();

Loading…
Cancel
Save