192 lines
5.9 KiB
Rust
192 lines
5.9 KiB
Rust
use std::{
|
|
collections::{
|
|
BTreeMap,
|
|
BTreeSet,
|
|
},
|
|
fs,
|
|
io,
|
|
path,
|
|
};
|
|
|
|
use crate::{
|
|
folders, input, manage::Specification
|
|
};
|
|
|
|
use anyhow::Context;
|
|
|
|
/// Rockbox writes a BOM to playlist files it creates, so we use this function
|
|
/// to extract the BOM if it exists, from a reader.
|
|
fn extract_bom<R: io::Read>(
|
|
reader: &mut io::BufReader<R>,
|
|
) -> anyhow::Result<()> {
|
|
use io::Read;
|
|
|
|
let mut bom = Vec::with_capacity(3);
|
|
let mut bom_reader = reader.take(3);
|
|
bom_reader.read_to_end(&mut bom)?;
|
|
|
|
if &bom[..] == &[0xEF, 0xBB, 0xBF] {
|
|
let mut dummy = [0u8; 3];
|
|
reader.read_exact(&mut dummy)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
|
|
/// Takes a playlist of listened files and updates the status of the files in
|
|
/// the master copy.
|
|
pub(crate) fn sync_listened(
|
|
// The folder containing the podcasts.toml file.
|
|
root: &path::Path,
|
|
// The path to the m3u playlist file.
|
|
playlist: &path::Path,
|
|
// Whether the playlist file given should be emptied when done.
|
|
empty_file: bool,
|
|
) -> anyhow::Result<()> {
|
|
// The way the specification file was written makes this operation much
|
|
// slower than it needs to be, since there isn't a convenient lookup from
|
|
// file to episodes, only the other way around.
|
|
//
|
|
// In practice it hardly matters though, the playlist can never really be
|
|
// that big.
|
|
|
|
if !playlist.exists() {
|
|
println!("[warning] specified playlist file does not exist. skipping.");
|
|
|
|
if empty_file {
|
|
fs::write(playlist, [])?;
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let mut reader =
|
|
io::BufReader::new(fs::File::open(playlist)?);
|
|
|
|
// Extract the BOM if it exists
|
|
extract_bom(&mut reader)?;
|
|
|
|
let mut reader = m3u::Reader::new(
|
|
reader
|
|
);
|
|
|
|
// We first extract all of the played files as a map from the podcast name
|
|
// to all of the played episodes of that podcast.
|
|
let mut played = BTreeMap::<String, BTreeSet<_>>::new();
|
|
for entry in reader.entries() {
|
|
if let m3u::Entry::Path(entry) = entry? {
|
|
|
|
let mut components: Vec<_> = entry.components().rev().take(2).collect();
|
|
|
|
let Some(podcast) = components.pop() else {
|
|
println!("[warning] could not identify podcast for file {:?}.", entry);
|
|
continue;
|
|
};
|
|
let Some(file_name) = components.pop() else {
|
|
println!("[warning] empty file name found in playlist. skipping.");
|
|
continue;
|
|
};
|
|
|
|
let podcast = podcast.as_os_str().to_str().context("podcast name was not a valid string")?;
|
|
let file_name = path::PathBuf::from(file_name.as_os_str());
|
|
|
|
// We append the episode to the appropriate entry of the map, or
|
|
// create a new one if we are looking at the first episode of a
|
|
// given podcast.
|
|
match played.get_mut(podcast) {
|
|
Some(existing) => {
|
|
existing.insert(file_name.to_owned());
|
|
}
|
|
None => {
|
|
played.insert(
|
|
podcast.to_owned(),
|
|
BTreeSet::from([file_name.to_owned()]),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (podcast, played) in played {
|
|
println!("[info] syncing played episodes for {}", podcast);
|
|
|
|
let output = folders::podcast_folder(root, podcast.as_str());
|
|
let spec_file = output.join(folders::SPEC_FILE);
|
|
let mut spec = Specification::read_from(&spec_file)?;
|
|
|
|
let (feed, files) = spec.files_and_feed_mut();
|
|
for (_, episodes) in feed.iter_mut() {
|
|
for episode in episodes {
|
|
if played.contains(files.get(episode.id()).as_deref().unwrap().as_ref()) {
|
|
println!("[info] marking {:?} as played", episode.title());
|
|
episode.listened = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
spec.write_to(&spec_file)?;
|
|
}
|
|
|
|
if empty_file {
|
|
fs::write(playlist, [])?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn sync(
|
|
root: &path::Path,
|
|
config: &input::Config,
|
|
target_dir: &path::Path,
|
|
) -> anyhow::Result<()> {
|
|
|
|
if !target_dir.exists() {
|
|
anyhow::bail!("specified target directory does not exist");
|
|
}
|
|
|
|
if !target_dir.join(folders::LOCAL_PODCASTS_DIR).exists() {
|
|
anyhow::bail!("specified target directory does not contain a folder {:?}", folders::LOCAL_PODCASTS_DIR);
|
|
}
|
|
|
|
for (podcast, _) in &config.podcasts {
|
|
let output = folders::podcast_folder(root, podcast.as_str());
|
|
let spec_file = output.join(folders::SPEC_FILE);
|
|
let spec = Specification::read_from(&spec_file)?;
|
|
|
|
for episode in spec.feed_iter().map(|(_, eps)| eps.iter()).flatten() {
|
|
let episode_local_path = spec.path_from_id(episode.id()).unwrap();
|
|
// let relative_path = path::PathBuf::from().join(podcast.as_str()).join(path);
|
|
|
|
let source = output.join(&episode_local_path);
|
|
let target = target_dir.join(source.strip_prefix(root).unwrap());
|
|
|
|
if !target.exists() {
|
|
println!("[info] copying from {:?} to {:?}.", source, target);
|
|
fs::create_dir_all(target.parent().unwrap())?;
|
|
fs::copy(source, target)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !target_dir.join(folders::LOCAL_PLAYLISTS_DIR).exists() {
|
|
anyhow::bail!("specified target directory does not contain a folder {:?}", folders::LOCAL_PLAYLISTS_DIR);
|
|
}
|
|
|
|
for source in fs::read_dir(root.join(folders::LOCAL_PLAYLISTS_DIR))? {
|
|
let source = source?.path();
|
|
let target = target_dir
|
|
.join(folders::LOCAL_PLAYLISTS_DIR)
|
|
.join(source.file_name().unwrap());
|
|
|
|
if !target.exists() || fs::metadata(&target)?.modified()? < fs::metadata(&source)?.modified()? {
|
|
println!("[info] copying playlist {:?}.", source.file_name().unwrap());
|
|
fs::copy(source, target)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
|
|
|