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( reader: &mut io::BufReader, ) -> 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::>::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(()) }