use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::config::Config;
use crate::db::queries;
use crate::player::commands::PlayerCommand;
use crate::player::state::SharedPlayerState;
use crate::remote::client::{SubsonicClient, SubsonicError};
use crate::remote::download::DownloadError;
use super::*;
pub(super) fn is_cached_audio(path: &std::path::Path) -> bool {
const MIN_PLAUSIBLE_BYTES: u64 = 4096;
match std::fs::metadata(path) {
Ok(meta) if meta.len() >= MIN_PLAUSIBLE_BYTES => true,
Ok(_) => {
let mut first = [0u8; 1];
match std::fs::File::open(path)
.and_then(|mut f| std::io::Read::read_exact(&mut f, &mut first).map(|_| first[0]))
{
Ok(b) => b != b'{' && b != b'<',
Err(_) => false,
}
}
Err(_) => false,
}
}
pub(crate) fn download_track(
db_id: i64,
cancelled: &dyn Fn() -> bool,
tx: &crossbeam_channel::Sender<PlayerCommand>,
state: &SharedPlayerState,
cfg: &Config,
client: &SubsonicClient,
) -> Option<Result<PathBuf, String>> {
let db = match crate::db::pool::shared().get() {
Ok(db) => db,
Err(e) => return Some(Err(format!("db error: {e}"))),
};
let Ok(Some(track)) = queries::get_track_row(&db.conn, db_id) else {
return Some(Err("track not found".into()));
};
if let Some(p) = track.path.as_deref().map(PathBuf::from)
&& p.exists()
{
log::info!("download_track: local file exists, using {}", p.display());
return Some(Ok(p));
}
let Some(remote_id) = track.remote_id.clone() else {
return Some(Err(
"not in the library folder, and no remote copy to fetch".into(),
));
};
let album_date: Option<String> = track
.album_id
.and_then(|aid| queries::album_date(&db.conn, aid).ok().flatten());
let cache_dir = cfg.cache_dir();
let dest = cache_path_for_track(&cache_dir, &track, album_date.as_deref());
if !path_within(&cache_dir, &dest) {
return Some(Err(format!(
"cache path escapes the cache: {}",
dest.display()
)));
}
if dest.exists() && !is_cached_audio(&dest) {
log::warn!(
"discarding non-audio cache entry {} (likely a stored server error)",
dest.display()
);
let _ = std::fs::remove_file(&dest);
}
if dest.exists() {
adopt_cached(&db.conn, &track, &dest);
return Some(Ok(dest));
}
let store = state.downloads();
let bytes_written = store.announce(
db_id,
track.title.clone(),
track.artist_name.clone(),
crate::remote::download::part_path(&dest),
dest.clone(),
);
let progress_tx = tx.clone();
let stream_ready_flag = std::sync::atomic::AtomicBool::new(false);
let announced_total = AtomicU64::new(u64::MAX);
let result =
client.download_with_progress(&remote_id, &dest, cancelled, |downloaded, total| {
bytes_written.set(downloaded);
store.progressed();
if announced_total.swap(total, Ordering::Relaxed) != total {
store.started(db_id, total);
}
if !stream_ready_flag.load(Ordering::Relaxed)
&& downloaded >= crate::player::state::STREAM_THRESHOLD
{
stream_ready_flag.store(true, Ordering::Relaxed);
for id in store.waiters(db_id) {
progress_tx.send(PlayerCommand::TrackStreamReady(id)).ok();
}
}
});
match result {
Err(SubsonicError::Download(DownloadError::Cancelled)) => None,
Err(e) => {
log::warn!("x {} — {}", track.title, e);
Some(Err(e.to_string()))
}
Ok(()) => {
cache_grew(std::fs::metadata(&dest).map_or(0, |m| m.len()));
if let Err(e) = queries::set_cached_path(&db.conn, db_id, &dest.to_string_lossy()) {
log::warn!(
"cached {} but failed to record it ({}) — it will not be evicted",
dest.display(),
e
);
}
log::info!("+ {} — {}", track.title, track.artist_name);
Some(Ok(dest))
}
}
}