use {
crate::{
client::E6Client,
error::{Report, Result},
models::{E6PoolResponse, E6PoolsResponse, E6PostsResponse},
},
chrono::{Datelike, Days, Local},
color_eyre::eyre::Context,
flate2::read::GzDecoder,
sha2::{Digest, Sha256},
std::{io::Read, path::Path},
tokio::fs,
tracing::{debug, info, instrument, warn},
};
impl E6Client {
#[instrument(skip(self), name = "update_pools_with")]
pub async fn update_pools_with(&self, local_file: &str) -> Result<()> {
let local_hash_file = format!("{}.hash", local_file);
let now = Local::now()
.checked_sub_days(Days::new(1))
.unwrap_or(Local::now());
let url = format!(
"https://e621.net/db_export/pools-{:04}-{:02}-{:02}.csv.gz",
now.year(),
now.month(),
now.day()
);
self.download_and_update_file(&url, local_file, &local_hash_file, "pools")
.await
}
#[cfg(feature = "cli")]
#[instrument(skip(self), name = "update_pools")]
pub async fn update_pools(&self) -> Result<()> {
self.update_pools_with(&crate::getopt!(completion.pools)).await
}
pub async fn download_and_update_file(
&self,
url: &str,
local_file: &str,
hash_file: &str,
file_type: &str,
) -> Result<()> {
let response = match self
.client
.get(url)
.send()
.await
.context(format!("failed to fetch {}", file_type))
{
Ok(r) => r,
Err(e) => {
warn!("couldn't download the update: {}", e);
return Ok(());
}
};
let remote_bytes = response.bytes().await?;
let remote_hash_hex = {
let mut hasher = Sha256::new();
hasher.update(&remote_bytes);
hex::encode(hasher.finalize())
};
let update_needed = match fs::read_to_string(hash_file).await {
Ok(local_hash) => local_hash.trim() != remote_hash_hex,
Err(_) => true,
};
if !update_needed {
info!(file_type, "Local snapshot is up to date");
return Ok(());
}
info!(file_type, "Updating local snapshot");
let mut gz = GzDecoder::new(&remote_bytes[..]);
let mut decompressed = Vec::new();
gz.read_to_end(&mut decompressed)
.with_context(|| format!("Failed to decompress {}", file_type))?;
if let Some(parent) = Path::new(local_file).parent() {
fs::create_dir_all(parent).await?;
}
let temp_file = format!("{}.tmp", local_file);
fs::write(&temp_file, &decompressed).await?;
fs::rename(&temp_file, local_file).await?;
fs::write(hash_file, remote_hash_hex.as_bytes()).await?;
info!(file_type, path = local_file, "Updated local snapshot");
Ok(())
}
#[instrument(skip(self), fields(limit))]
pub async fn get_pools(&self, limit: Option<u64>) -> Result<E6PoolsResponse> {
let limit = limit.unwrap_or(20).min(320);
let url = format!("{}/pools.json?limit={}", self.base_url, limit);
let bytes = self.get_cached_or_fetch(&url).await?;
serde_json::from_slice(&bytes)
.context("Failed to deserialize pools response")
.map_err(Report::new)
}
#[instrument(skip(self))]
pub async fn get_pool_by_id(&self, id: i64) -> Result<E6PoolResponse> {
let url = format!("{}/pools/{}.json", self.base_url, id);
let bytes = self.get_cached_or_fetch(&url).await?;
serde_json::from_slice(&bytes)
.with_context(|| format!("Failed to deserialize pool {}", id))
.map_err(Report::new)
}
#[instrument(skip(self))]
pub async fn get_pool_posts(&self, pool_id: i64) -> Result<E6PostsResponse> {
let url = format!("{}/posts.json?tags=pool:{}", self.base_url, pool_id);
let bytes = self.get_cached_or_fetch(&url).await?;
let posts: E6PostsResponse =
serde_json::from_slice(&bytes).context("Failed to deserialize pool posts")?;
debug!(pool_id, count = posts.posts.len(), "Fetched pool posts");
Ok(posts)
}
#[instrument(skip(self))]
pub async fn search_pools(&self, query: &str, limit: Option<u64>) -> Result<E6PoolsResponse> {
self.search_pools_internal("name_matches", query, limit)
.await
}
#[instrument(skip(self))]
pub async fn search_pools_by_creator(
&self,
creator_name: &str,
limit: Option<u64>,
) -> Result<E6PoolsResponse> {
self.search_pools_internal("creator_name", creator_name, limit)
.await
}
#[instrument(skip(self))]
pub async fn search_pools_by_description(
&self,
query: &str,
limit: Option<u64>,
) -> Result<E6PoolsResponse> {
self.search_pools_internal("description_matches", query, limit)
.await
}
#[bearive::argdoc]
pub async fn search_pools_internal(
&self,
search_type: &str,
query: &str,
limit: Option<u64>,
) -> Result<E6PoolsResponse> {
let limit = limit.unwrap_or(20).min(320);
let url = format!(
"{}/pools.json?search[{}]={}&limit={}",
self.base_url,
search_type,
urlencoding::encode(query),
limit
);
debug!(url, "Searching pools");
let bytes = self.get_cached_or_fetch(&url).await?;
Ok(serde_json::from_slice(&bytes).context("Failed to deserialize pools response")?)
}
}