use std::fs::File;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::{Context as _, Result, anyhow};
use bywind::{
fetch::{FetchProgress, FetchSpec, fetch_to_grib2, parse_yyyymmddhh, transcode_grib2_to_wcav},
io::Format,
};
use chrono::{DateTime, Utc};
use crate::error::AppError;
#[derive(clap::Args, Debug)]
pub struct FetchArgs {
pub start: String,
pub end: String,
#[arg(long, short = 'o')]
pub out: PathBuf,
#[arg(long, value_name = "N", default_value_t = 1)]
pub interval_h: u32,
}
pub fn run(args: &FetchArgs) -> Result<(), AppError> {
let start = parse_yyyymmddhh(&args.start)
.map_err(|e| anyhow!(e))
.context("--start")?;
let end = parse_yyyymmddhh(&args.end)
.map_err(|e| anyhow!(e))
.context("--end")?;
let spec = FetchSpec {
start,
end,
interval_h: args.interval_h,
};
let out_fmt = Format::from_path(&args.out).context("output path")?;
eprintln!(
"fetch: {} → {} (interval {} h) → {}",
format_when(start),
format_when(end),
args.interval_h,
args.out.display(),
);
let t0 = Instant::now();
let stats = match out_fmt {
Format::Grib2 => fetch_direct(&spec, &args.out)?,
Format::WindAv1 => fetch_then_encode_wcav(&spec, &args.out)?,
};
eprintln!(
"\ndone: {} frames, {} skipped, {} MB in {:.1}s",
stats.fetched,
stats.skipped,
stats.total_bytes / (1024 * 1024),
t0.elapsed().as_secs_f64(),
);
Ok(())
}
fn fetch_direct(spec: &FetchSpec, out: &Path) -> Result<bywind::fetch::FetchStats> {
let file = File::create(out).with_context(|| format!("creating {}", out.display()))?;
let mut writer = BufWriter::new(file);
let stats = fetch_to_grib2(spec, &mut writer, log_progress)?;
Ok(stats)
}
fn fetch_then_encode_wcav(spec: &FetchSpec, out: &Path) -> Result<bywind::fetch::FetchStats> {
let staging = out.with_extension("grib2.tmp");
let stats = {
let file =
File::create(&staging).with_context(|| format!("creating {}", staging.display()))?;
let mut writer = BufWriter::new(file);
fetch_to_grib2(spec, &mut writer, log_progress)?
};
eprintln!("\nencoding {} as wind_av1...", out.display());
let t0 = Instant::now();
transcode_grib2_to_wcav(&staging, out).map_err(anyhow::Error::from)?;
eprintln!(" encoded in {:.1}s", t0.elapsed().as_secs_f64());
if let Err(e) = std::fs::remove_file(&staging) {
eprintln!(
"note: failed to delete staging file {}: {e}",
staging.display()
);
}
Ok(stats)
}
fn log_progress(ev: FetchProgress) -> std::ops::ControlFlow<()> {
match ev {
FetchProgress::Fetched {
idx,
total,
timestamp,
bytes,
} => eprintln!(
"[{idx:3}/{total:3}] {} ok ({} KB)",
format_when(timestamp),
bytes / 1024,
),
FetchProgress::Skipped {
idx,
total,
timestamp,
reason,
} => eprintln!(
"[{idx:3}/{total:3}] {} skipped: {reason}",
format_when(timestamp),
),
}
std::ops::ControlFlow::Continue(())
}
fn format_when(t: DateTime<Utc>) -> String {
t.format("%Y-%m-%d %H:%M UTC").to_string()
}
impl From<bywind::fetch::FetchError> for AppError {
fn from(e: bywind::fetch::FetchError) -> Self {
use bywind::fetch::FetchError;
match e {
FetchError::ForecastHourOutOfRange { .. } => Self::internal(anyhow!(e)),
FetchError::NoFramesFetched { .. } => Self::no_result(anyhow!(e)),
_ => Self::from(anyhow!(e)),
}
}
}