diff --git a/Cargo.toml b/Cargo.toml index a4f09d6..6f3c35f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hydrus-api" -version = "0.4.0" +version = "0.5.0" authors = ["trivernis "] edition = "2018" license = "Apache-2.0" diff --git a/README.md b/README.md index 9041978..04d2258 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ use hydrus_api::wrapper::tag::Tag; use hydrus_api::wrapper::service::ServiceName; use hydrus_api::wrapper::hydrus_file::FileStatus; use hydrus_api::wrapper::page::PageIdentifier; +use hydrus_api::wrapper::builders::search_builder::SortType; use hydrus_api::wrapper::builders::tag_builder::{ SystemTagBuilder, Comparator }; @@ -33,11 +34,12 @@ async fn main() { let access_key = env::var("HYDRUS_ACCESS_KEY").unwrap(); let hydrus = Hydrus::new(Client::new(hydrus_url, access_key)); - let files = hydrus.search(vec![ - Tag::from("character:megumin"), - SystemTagBuilder::new().archive().build(), - SystemTagBuilder::new().number_of_tags(Comparator::Greater, 12).build(), - ]).await.unwrap(); + let files = hydrus.search() + .add_tag(Tag::from("character:megumin")) + .add_tag(SystemTagBuilder::new().archive().build()) + .add_tag(SystemTagBuilder::new().number_of_tags(Comparator::Greater, 12).build()) + .sort(SortType::ModifiedTime) + .run().await.unwrap(); for mut file in files { file.add_tags(ServiceName::my_tags(), vec![Tag::from("ark mage")]).await.unwrap(); diff --git a/src/api_core/client.rs b/src/api_core/client.rs index abe4391..f3b288f 100644 --- a/src/api_core/client.rs +++ b/src/api_core/client.rs @@ -21,7 +21,8 @@ use crate::api_core::managing_pages::{ GetPages, GetPagesResponse, }; use crate::api_core::searching_and_fetching_files::{ - FileMetadata, FileMetadataResponse, GetFile, SearchFiles, SearchFilesResponse, + FileMetadata, FileMetadataResponse, FileSearchOptions, GetFile, SearchFiles, + SearchFilesResponse, }; use crate::api_core::Endpoint; use crate::error::{Error, Result}; @@ -222,13 +223,16 @@ impl Client { } /// Searches for files in the inbox, the archive or both - pub async fn search_files(&self, tags: Vec) -> Result { + pub async fn search_files( + &self, + tags: Vec, + options: FileSearchOptions, + ) -> Result { log::trace!("Searching for files with tags {:?}", tags); - self.get_and_parse::(&[( - "tags", - string_list_to_json_array(tags), - )]) - .await + let mut args = options.into_query_args(); + args.push(("tags", string_list_to_json_array(tags))); + self.get_and_parse::(&args) + .await } /// Returns the metadata for a given list of file_ids or hashes diff --git a/src/api_core/mod.rs b/src/api_core/mod.rs index b046348..9f87869 100644 --- a/src/api_core/mod.rs +++ b/src/api_core/mod.rs @@ -10,6 +10,7 @@ pub mod common; pub mod managing_cookies_and_http_headers; pub mod managing_pages; pub mod searching_and_fetching_files; +pub use searching_and_fetching_files::file_sort_type; pub(crate) trait Endpoint { type Request: Serialize; diff --git a/src/api_core/searching_and_fetching_files.rs b/src/api_core/searching_and_fetching_files.rs index 9b47857..677858c 100644 --- a/src/api_core/searching_and_fetching_files.rs +++ b/src/api_core/searching_and_fetching_files.rs @@ -1,6 +1,101 @@ use crate::api_core::common::FileMetadataInfo; use crate::api_core::Endpoint; +pub mod file_sort_type { + pub const SORT_FILE_SIZE: u8 = 0; + pub const SORT_FILE_DURATION: u8 = 1; + pub const SORT_FILE_IMPORT_TIME: u8 = 2; + pub const SORT_FILE_TYPE: u8 = 3; + pub const SORT_FILE_RANDOM: u8 = 4; + pub const SORT_FILE_WIDTH: u8 = 5; + pub const SORT_FILE_HEIGHT: u8 = 6; + pub const SORT_FILE_RATIO: u8 = 7; + pub const SORT_FILE_PIXEL_COUNT: u8 = 8; + pub const SORT_FILE_TAG_COUNT: u8 = 9; + pub const SORT_FILE_MEDIA_VIEWS: u8 = 10; + pub const SORT_FILE_MEDIA_VIEWTIME: u8 = 11; + pub const SORT_FILE_BITRATE: u8 = 12; + pub const SORT_FILE_HAS_AUDIO: u8 = 13; + pub const SORT_FILE_MODIFIED_TIME: u8 = 14; + pub const SORT_FILE_FRAMERATE: u8 = 15; + pub const SORT_FILE_FRAME_COUNT: u8 = 16; +} + +#[derive(Clone, Debug, Default)] +pub struct FileSearchOptions { + file_service_name: Option, + file_service_key: Option, + tag_service_name: Option, + tag_service_key: Option, + file_sort_type: Option, + file_sort_asc: Option, +} + +impl FileSearchOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn file_service_name(mut self, name: S) -> Self { + self.file_service_name = Some(name.to_string()); + self + } + + pub fn file_service_key(mut self, key: S) -> Self { + self.file_service_key = Some(key.to_string()); + self + } + + pub fn tag_service_name(mut self, name: S) -> Self { + self.tag_service_name = Some(name.to_string()); + self + } + + pub fn tag_service_key(mut self, key: S) -> Self { + self.tag_service_key = Some(key.to_string()); + self + } + + pub fn sort_type(mut self, sort_type: u8) -> Self { + self.file_sort_type = Some(sort_type); + self + } + + pub fn asc(mut self) -> Self { + self.file_sort_asc = Some(true); + self + } + + pub fn desc(mut self) -> Self { + self.file_sort_asc = Some(false); + self + } + + pub(crate) fn into_query_args(self) -> Vec<(&'static str, String)> { + let mut args = Vec::new(); + if let Some(sort) = self.file_sort_type { + args.push(("file_sort_type", sort.to_string())); + } + if let Some(file_service_name) = self.file_service_name { + args.push(("file_service_name", file_service_name)); + } + if let Some(file_service_key) = self.file_service_key { + args.push(("file_service_key", file_service_key)); + } + if let Some(tag_service_name) = self.tag_service_name { + args.push(("tag_service_name", tag_service_name)) + } + if let Some(tag_service_key) = self.tag_service_key { + args.push(("tag_service_key", tag_service_key)); + } + if let Some(sort_asc) = self.file_sort_asc { + args.push(("file_sort_asc", sort_asc.to_string())) + } + + args + } +} + #[derive(Debug, Clone, Deserialize)] pub struct SearchFilesResponse { pub file_ids: Vec, diff --git a/src/lib.rs b/src/lib.rs index cbf98e5..c9f8f5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,17 +13,20 @@ //! use hydrus_api::wrapper::hydrus_file::FileStatus; //! use hydrus_api::wrapper::page::PageIdentifier; //! use hydrus_api::wrapper::builders::tag_builder::{SystemTagBuilder, Comparator}; +//! use hydrus_api::wrapper::builders::search_builder::SortType; //! //! # #[tokio::test] //! # async fn doctest() { //! let hydrus_url = env::var("HYDRUS_URL").unwrap(); //! let access_key = env::var("HYDRUS_ACCESS_KEY").unwrap(); //! let hydrus = Hydrus::new(Client::new(hydrus_url, access_key)); -//! let files = hydrus.search(vec![ -//! Tag::from("character:megumin"), -//! SystemTagBuilder::new().archive().build(), -//! SystemTagBuilder::new().tag_namespace_as_number("page", Comparator::Equal, 5).negate().build(), -//! ]).await.unwrap(); +//! let files = hydrus.search() +//! .add_tag(Tag::from("character:megumin")) +//! .add_tag(SystemTagBuilder::new().archive().build()) +//! .add_tag(SystemTagBuilder::new().tag_namespace_as_number("page", Comparator::Equal, 5).negate().build()) +//! .sort_by(SortType::NumberOfPixels) +//! .sort_descending() +//! .run().await.unwrap(); //! //! for mut file in files { //! file.add_tags(ServiceName::my_tags(), vec![Tag::from("ark mage")]).await.unwrap(); @@ -74,5 +77,5 @@ pub use wrapper::hydrus::Hydrus; pub mod api_core; pub mod error; -pub(crate) mod utils; +pub mod utils; pub mod wrapper; diff --git a/src/utils.rs b/src/utils.rs index e615d37..2fa9107 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,12 @@ +use crate::api_core::common::FileIdentifier; use crate::wrapper::tag::Tag; use chrono::{Datelike, Duration}; -pub fn string_list_to_json_array(l: Vec) -> String { +pub(crate) fn string_list_to_json_array(l: Vec) -> String { format!("[\"{}\"]", l.join("\",\"")) } -pub fn number_list_to_json_array(l: Vec) -> String { +pub(crate) fn number_list_to_json_array(l: Vec) -> String { format!( "[{}]", l.into_iter() @@ -23,7 +24,7 @@ pub fn tag_list_to_string_list(tags: Vec) -> Vec { tags.into_iter().map(|t| t.to_string()).collect() } -pub fn format_datetime(datetime: D) -> String { +pub(crate) fn format_datetime(datetime: D) -> String { format!( "{:04}-{:02}-{:02}", datetime.year(), @@ -32,7 +33,7 @@ pub fn format_datetime(datetime: D) -> String { ) } -pub fn format_duration(duration: Duration) -> String { +pub(crate) fn format_duration(duration: Duration) -> String { let mut expression = String::new(); let days = duration.num_days(); let hours = duration.num_hours() % 24; @@ -56,3 +57,18 @@ pub fn format_duration(duration: Duration) -> String { expression } + +pub(crate) fn split_file_identifiers_into_hashes_and_ids( + files: Vec, +) -> (Vec, Vec) { + let mut ids = Vec::new(); + let mut hashes = Vec::new(); + + for file in files { + match file { + FileIdentifier::ID(id) => ids.push(id), + FileIdentifier::Hash(hash) => hashes.push(hash), + } + } + (ids, hashes) +} diff --git a/src/wrapper/builders/mod.rs b/src/wrapper/builders/mod.rs index 7ab8348..0584473 100644 --- a/src/wrapper/builders/mod.rs +++ b/src/wrapper/builders/mod.rs @@ -1,3 +1,4 @@ pub mod import_builder; pub mod tagging_builder; pub mod tag_builder; +pub mod search_builder; diff --git a/src/wrapper/builders/search_builder.rs b/src/wrapper/builders/search_builder.rs new file mode 100644 index 0000000..7c6c35f --- /dev/null +++ b/src/wrapper/builders/search_builder.rs @@ -0,0 +1,115 @@ +use crate::api_core::searching_and_fetching_files::FileSearchOptions; +use crate::error::Result; +use crate::utils::tag_list_to_string_list; +use crate::wrapper::hydrus_file::HydrusFile; +use crate::wrapper::service::ServiceName; +use crate::wrapper::tag::Tag; +use crate::Client; + +pub enum SortType { + FileSize, + Duration, + ImportTime, + FileType, + Random, + Width, + Height, + Ratio, + NumberOfPixels, + NumberOfTags, + NumberOfMediaViewers, + MediaViewTime, + Bitrate, + HasAudio, + ModifiedTime, + Framerate, + NumberOfFrames, +} + +#[derive(Clone, Debug)] +pub struct SearchBuilder { + client: Client, + tags: Vec, + options: FileSearchOptions, +} + +impl SearchBuilder { + pub(crate) fn new(client: Client) -> Self { + Self { + client, + tags: Vec::new(), + options: FileSearchOptions::new(), + } + } + + /// Add multiple tags to filter by + pub fn add_tags(mut self, mut tags: Vec) -> Self { + self.tags.append(&mut tags); + self + } + + /// Add a tag to filter by + pub fn add_tag(mut self, tag: Tag) -> Self { + self.tags.push(tag); + self + } + + /// Sets the sort type + pub fn sort_by(mut self, sort_type: SortType) -> Self { + self.options = self.options.sort_type(sort_type as u8); + self + } + + /// Sorts descending + pub fn sort_descending(mut self) -> Self { + self.options = self.options.desc(); + self + } + + /// Sorts ascending + pub fn sort_ascending(mut self) -> Self { + self.options = self.options.asc(); + self + } + + /// Sets the file service name to search in + pub fn file_service_name(mut self, service: ServiceName) -> Self { + self.options = self.options.file_service_name(service); + self + } + + /// Sets the tag service to search by + pub fn tag_service_name(mut self, service: ServiceName) -> Self { + self.options = self.options.tag_service_name(service); + self + } + + /// Sets the file service key. This option is preferred over + /// setting it by name because it's faster + pub fn file_service_key(mut self, key: S) -> Self { + self.options = self.options.file_service_key(key); + self + } + + /// Sets the tag service key. This option is preferred over + /// setting it by name because it's faster + pub fn tag_service_key(mut self, key: S) -> Self { + self.options = self.options.tag_service_key(key); + self + } + + /// Runs the search + pub async fn run(self) -> Result> { + let client = self.client.clone(); + let response = client + .search_files(tag_list_to_string_list(self.tags), self.options) + .await?; + let files = response + .file_ids + .into_iter() + .map(|id| HydrusFile::from_id(client.clone(), id)) + .collect(); + + Ok(files) + } +} diff --git a/src/wrapper/hydrus.rs b/src/wrapper/hydrus.rs index 39dd6f9..031ee28 100644 --- a/src/wrapper/hydrus.rs +++ b/src/wrapper/hydrus.rs @@ -1,13 +1,12 @@ use crate::api_core::common::FileIdentifier; use crate::error::Result; -use crate::utils::tag_list_to_string_list; use crate::wrapper::address::Address; use crate::wrapper::builders::import_builder::ImportBuilder; +use crate::wrapper::builders::search_builder::SearchBuilder; use crate::wrapper::builders::tagging_builder::TaggingBuilder; use crate::wrapper::hydrus_file::HydrusFile; use crate::wrapper::page::HydrusPage; use crate::wrapper::service::Services; -use crate::wrapper::tag::Tag; use crate::wrapper::url::Url; use crate::wrapper::version::Version; use crate::Client; @@ -82,19 +81,9 @@ impl Hydrus { TaggingBuilder::new(self.client.clone()) } - /// Searches for files that have the given tags and returns a list of hydrus files as a result - pub async fn search(&self, tags: Vec) -> Result> { - let search_result = self - .client - .search_files(tag_list_to_string_list(tags)) - .await?; - let files = search_result - .file_ids - .into_iter() - .map(|id| HydrusFile::from_id(self.client.clone(), id)) - .collect(); - - Ok(files) + /// Starts a request to search for files + pub fn search(&self) -> SearchBuilder { + SearchBuilder::new(self.client.clone()) } /// Returns a hydrus page by page key diff --git a/src/wrapper/hydrus_file.rs b/src/wrapper/hydrus_file.rs index 12036cc..a0312ea 100644 --- a/src/wrapper/hydrus_file.rs +++ b/src/wrapper/hydrus_file.rs @@ -18,6 +18,16 @@ pub enum FileStatus { impl Eq for FileStatus {} +impl From 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, @@ -41,17 +51,10 @@ impl HydrusFile { status: u8, hash: S, ) -> Self { - let status = if status == 3 { - FileStatus::Deleted - } else if status == 0 { - FileStatus::ReadyForImport - } else { - FileStatus::InDatabase - }; Self { client, id: FileIdentifier::Hash(hash.to_string()), - status, + status: status.into(), metadata: None, } } diff --git a/src/wrapper/page.rs b/src/wrapper/page.rs index 93669a2..48f37ea 100644 --- a/src/wrapper/page.rs +++ b/src/wrapper/page.rs @@ -1,5 +1,6 @@ use crate::api_core::common::{FileIdentifier, PageInformation}; use crate::error::Result; +use crate::utils::split_file_identifiers_into_hashes_and_ids; use crate::Client; #[derive(Clone)] @@ -40,27 +41,26 @@ impl HydrusPage { /// Adds files to a page pub async fn add_files(&self, files: Vec) -> Result<()> { - let mut hashes = Vec::new(); - let mut ids = Vec::new(); + let (ids, mut hashes) = split_file_identifiers_into_hashes_and_ids(files); - for file in files { - match file { - FileIdentifier::ID(id) => ids.push(id), - FileIdentifier::Hash(hash) => hashes.push(hash), - } - } // resolve file ids to hashes - if ids.len() > 0 && hashes.len() > 0 { - while let Some(id) = ids.pop() { - let metadata = self - .client - .get_file_metadata_by_identifier(FileIdentifier::ID(id)) - .await?; - hashes.push(metadata.hash); - } - } + hashes.append(&mut self.resolve_file_ids_to_hashes(ids).await?); - self.client.add_files_to_page(&self.key, ids, hashes).await + self.client + .add_files_to_page(&self.key, [].to_vec(), hashes) + .await + } + + async fn resolve_file_ids_to_hashes(&self, ids: Vec) -> Result> { + let mut hashes = Vec::new(); + for id in ids { + let metadata = self + .client + .get_file_metadata_by_identifier(FileIdentifier::ID(id)) + .await?; + hashes.push(metadata.hash); + } + Ok(hashes) } } diff --git a/src/wrapper/service.rs b/src/wrapper/service.rs index 0e0b36b..f85998e 100644 --- a/src/wrapper/service.rs +++ b/src/wrapper/service.rs @@ -4,7 +4,9 @@ use crate::api_core::access_management::{ SERVICE_TYPE_FILE_REPOSITORIES, SERVICE_TYPE_LOCAL_FILES, SERVICE_TYPE_LOCAL_TAGS, SERVICE_TYPE_TAG_REPOSITORIES, SERVICE_TYPE_TRASH, }; + use crate::error::Error; +use crate::wrapper::builders::search_builder::SearchBuilder; use crate::Client; use std::collections::HashMap; use std::convert::TryFrom; @@ -102,6 +104,22 @@ pub struct Service { pub service_type: ServiceType, } +impl Service { + pub fn search(&self) -> SearchBuilder { + let builder = SearchBuilder::new(self.client.clone()); + match self.service_type { + ServiceType::LocalTags | ServiceType::TagRepositories | ServiceType::AllKnownTags => { + builder.tag_service_key(&self.key) + } + ServiceType::LocalFiles + | ServiceType::FileRepositories + | ServiceType::AllLocalFiles + | ServiceType::AllKnownFiles + | ServiceType::Trash => builder.file_service_key(&self.key), + } + } +} + #[derive(Clone)] pub struct Services { inner: HashMap>, @@ -140,13 +158,9 @@ impl Services { /// Returns a list of all services of the given type pub fn get_services(&self, service_type: ServiceType) -> Vec<&Service> { if let Some(services) = self.inner.get(&service_type) { - let mut borrowed_services = Vec::with_capacity(services.len()); - for service in services { - borrowed_services.push(service) - } - borrowed_services + services.into_iter().collect() } else { - Vec::with_capacity(0) + Vec::new() } } } diff --git a/tests/client/test_searching_and_fetching_files.rs b/tests/client/test_searching_and_fetching_files.rs index 4289443..9859b6b 100644 --- a/tests/client/test_searching_and_fetching_files.rs +++ b/tests/client/test_searching_and_fetching_files.rs @@ -1,11 +1,17 @@ use super::super::common; use hydrus_api::api_core::common::FileIdentifier; +use hydrus_api::api_core::file_sort_type::SORT_FILE_PIXEL_COUNT; +use hydrus_api::api_core::searching_and_fetching_files::FileSearchOptions; #[tokio::test] async fn is_searches_files() { let client = common::get_client(); + let options = FileSearchOptions::new() + .sort_type(SORT_FILE_PIXEL_COUNT) + .tag_service_name("public tag repository") + .file_service_name("all known files"); client - .search_files(vec!["beach".to_string()]) + .search_files(vec!["beach".to_string()], options) .await .unwrap(); } diff --git a/tests/wrapper/mod.rs b/tests/wrapper/mod.rs index 9102ce3..dc843f3 100644 --- a/tests/wrapper/mod.rs +++ b/tests/wrapper/mod.rs @@ -1,7 +1,8 @@ +mod test_address; mod test_files; mod test_hydrus; mod test_import; -mod test_url; mod test_page; -mod test_address; +mod test_service; mod test_tags; +mod test_url; diff --git a/tests/wrapper/test_hydrus.rs b/tests/wrapper/test_hydrus.rs index 525c203..e4866b8 100644 --- a/tests/wrapper/test_hydrus.rs +++ b/tests/wrapper/test_hydrus.rs @@ -1,5 +1,6 @@ use super::super::common; use hydrus_api::api_core::adding_tags::TagAction; +use hydrus_api::wrapper::builders::search_builder::SortType; use hydrus_api::wrapper::service::{ServiceName, ServiceType}; use hydrus_api::wrapper::url::UrlType; @@ -36,7 +37,10 @@ async fn it_retrieves_url_information() { async fn it_searches() { let hydrus = common::get_hydrus(); hydrus - .search(vec!["character:megumin".into()]) + .search() + .add_tag("character:megumin".into()) + .sort_by(SortType::ModifiedTime) + .run() .await .unwrap(); } diff --git a/tests/wrapper/test_service.rs b/tests/wrapper/test_service.rs new file mode 100644 index 0000000..8949675 --- /dev/null +++ b/tests/wrapper/test_service.rs @@ -0,0 +1,27 @@ +use super::super::common; +use hydrus_api::wrapper::service::{Service, ServiceType, Services}; + +async fn get_services() -> Services { + let hydrus = common::get_hydrus(); + hydrus.services().await.unwrap() +} + +async fn get_file_service() -> Service { + let services = get_services().await; + services + .get_services(ServiceType::LocalFiles) + .pop() + .unwrap() + .clone() +} + +#[tokio::test] +async fn it_searches_for_files() { + let service = get_file_service().await; + service + .search() + .add_tag("character:rimuru tempest".into()) + .run() + .await + .unwrap(); +} diff --git a/tests/wrapper/test_tags.rs b/tests/wrapper/test_tags.rs index e51e3a6..5d0580c 100644 --- a/tests/wrapper/test_tags.rs +++ b/tests/wrapper/test_tags.rs @@ -10,7 +10,7 @@ use hydrus_api::wrapper::tag::Tag; async fn retrieve_single_tag(tag: Tag) -> Result<()> { let hydrus = common::get_hydrus(); - hydrus.search(vec![tag]).await?; + hydrus.search().add_tag(tag).run().await?; Ok(()) }