ipod syncing; updated readme; rust 2024; version bump
This commit is contained in:
190
src/sync.rs
Normal file
190
src/sync.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
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::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(())
|
||||
}
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user