use crate::file_handler::{FileFormat, FileHandler};
use crate::types::Timestamped;
pub trait LimitBuilder: Sized {
#[doc(hidden)]
fn limit_mut(&mut self) -> &mut Option<u32>;
#[must_use]
fn limit(mut self, n: u32) -> Self {
*self.limit_mut() = Some(n);
self
}
#[must_use]
fn unlimited(mut self) -> Self {
*self.limit_mut() = None;
self
}
}
#[allow(async_fn_in_trait)]
pub trait FetchAndSave: Sized {
type Item: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static;
fn resource_label() -> &'static str;
fn latest_timestamp(_items: &[Self::Item]) -> Option<u32> {
None
}
#[doc(hidden)]
async fn do_fetch(self) -> crate::error::Result<Vec<Self::Item>>;
async fn fetch_and_save(
self,
format: FileFormat,
filename_prefix: &str,
) -> crate::error::Result<String> {
let items = self.do_fetch().await?;
tracing::info!("Saving {} {} to file", items.len(), Self::resource_label());
let filename = FileHandler::save(&items, &format, filename_prefix)
.map_err(crate::error::LastFmError::Io)?;
if let Some(latest_ts) = Self::latest_timestamp(&items) {
FileHandler::write_sidecar_timestamp(&filename, latest_ts)
.map_err(crate::error::LastFmError::Io)?;
}
Ok(filename)
}
#[cfg(feature = "sqlite")]
async fn fetch_and_save_sqlite(self, filename_prefix: &str) -> crate::error::Result<String>
where
Self::Item: crate::sqlite::SqliteExportable,
{
let items = self.do_fetch().await?;
tracing::info!(
"Saving {} {} to SQLite",
items.len(),
Self::resource_label()
);
FileHandler::save_sqlite(&items, filename_prefix).map_err(crate::error::LastFmError::Io)
}
}
#[allow(async_fn_in_trait)]
pub trait FetchAndUpdate: Sized {
type Item: serde::Serialize
+ serde::de::DeserializeOwned
+ Timestamped
+ Clone
+ Send
+ Sync
+ 'static;
async fn fetch_since(self, max_ts: Option<u32>) -> crate::error::Result<Vec<Self::Item>>;
async fn fetch_and_update(self, file_path: &str) -> crate::error::Result<usize> {
let ext = std::path::Path::new(file_path)
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase);
let is_csv = ext.as_deref() == Some("csv");
let is_ndjson = ext.as_deref() == Some("ndjson");
let max_ts = if let Some(ts) = FileHandler::read_sidecar_timestamp(file_path) {
Some(ts)
} else if !is_csv && !is_ndjson && std::path::Path::new(file_path).exists() {
let existing: Vec<Self::Item> =
FileHandler::load(file_path).map_err(crate::error::LastFmError::Io)?;
let ts = existing.iter().filter_map(Timestamped::get_timestamp).max();
if let Some(t) = ts {
FileHandler::write_sidecar_timestamp(file_path, t)
.map_err(crate::error::LastFmError::Io)?;
}
ts
} else {
None
};
let new_items = self.fetch_since(max_ts).await?;
let count = new_items.len();
if !new_items.is_empty() {
if let Some(latest_ts) = new_items.first().and_then(Timestamped::get_timestamp) {
FileHandler::write_sidecar_timestamp(file_path, latest_ts)
.map_err(crate::error::LastFmError::Io)?;
}
if is_csv {
FileHandler::append_or_create_csv(&new_items, file_path)
.map_err(crate::error::LastFmError::Io)?;
} else if is_ndjson {
FileHandler::append_or_create_ndjson(&new_items, file_path)
.map_err(crate::error::LastFmError::Io)?;
} else {
FileHandler::prepend_json(&new_items, file_path)
.map_err(crate::error::LastFmError::Io)?;
}
}
Ok(count)
}
#[cfg(feature = "sqlite")]
async fn fetch_and_update_sqlite(self, db_path: &str) -> crate::error::Result<usize>
where
Self::Item: crate::sqlite::SqliteExportable,
{
let max_ts = FileHandler::read_sqlite_max_timestamp(
db_path,
<Self::Item as crate::sqlite::SqliteExportable>::table_name(),
);
let new_items = self.fetch_since(max_ts).await?;
let count = new_items.len();
if !new_items.is_empty() {
FileHandler::append_or_create_sqlite(&new_items, db_path)
.map_err(crate::error::LastFmError::Io)?;
}
Ok(count)
}
}
#[allow(async_fn_in_trait)]
pub trait Analyze: Sized {
type Item: crate::analytics::TrackAnalyzable;
#[doc(hidden)]
async fn do_fetch_for_analyze(self) -> crate::error::Result<Vec<Self::Item>>;
async fn analyze(self, threshold: usize) -> crate::error::Result<crate::analytics::TrackStats> {
let items = self.do_fetch_for_analyze().await?;
Ok(crate::analytics::AnalysisHandler::analyze_tracks(
&items, threshold,
))
}
async fn analyze_and_print(self, threshold: usize) -> crate::error::Result<()> {
let stats = self.analyze(threshold).await?;
crate::analytics::AnalysisHandler::print_analysis(&stats);
Ok(())
}
}
impl<T> Analyze for T
where
T: FetchAndSave,
T::Item: crate::analytics::TrackAnalyzable,
{
type Item = T::Item;
async fn do_fetch_for_analyze(self) -> crate::error::Result<Vec<Self::Item>> {
self.do_fetch().await
}
}