use std::{ fs, io, iter, path, collections::BTreeMap, }; use crate::{ input, folders, manage::Specification, }; use anyhow::Context; use sanitise_file_name::sanitise; struct Playlist<'a> { root: &'a path::Path, files: BTreeMap>, } impl<'a> Playlist<'a> { fn new(root: &'a path::Path) -> Self { Self { root, files: BTreeMap::new(), } } /// Writes the playlist file based on the specified filename. /// /// Output boolean indicates if the playlist was written (if it was /// different from the existing file) fn write_as( &self, name: &str, reverse: bool, ) -> anyhow::Result { let playlists_folder = self.root.join(folders::LOCAL_PLAYLISTS_DIR); if !playlists_folder.exists() { fs::create_dir(&playlists_folder) .context(format!("failed to create output directory for playlists"))?; } let mut path = playlists_folder.join(sanitise(name)); path.set_extension("m3u"); let mut output = io::BufWriter::new(Vec::new()); let mut writer = m3u::Writer::new(&mut output); let entries = self .files .values() .map(|entries| entries.iter()) .flatten(); if reverse { for entry in entries.rev() { writer.write_entry(entry)?; } } else { for entry in entries { writer.write_entry(entry)?; } }; drop(writer); let output = output.into_inner()?; if fs::read(&path)? != output { fs::write(path, output.as_slice())?; Ok(true) } else { Ok(false) } } fn insert( &mut self, published: chrono::NaiveDateTime, absolute_path: &path::Path, ) { let entry = m3u::path_entry({ let relative = absolute_path.strip_prefix( &self.root.join(folders::LOCAL_PODCASTS_DIR) ).unwrap(); path::Path::new(folders::IPOD_PODCASTS_DIR).join(relative) }); match self.files.get_mut(&published) { Some(existing) => { existing.push(entry) }, None => { self.files.insert( published, vec![entry] ); } } } } pub(crate) fn generate_master_m3u( config: &input::Config, root: &path::Path, ) -> anyhow::Result<()> { let mut playlist = Playlist::new(root); 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)?; let (feed, files) = spec.into_feed_and_files(); for (published, episodes) in feed { for episode in episodes { let path = output.join( files.get(episode.id()).unwrap() ); playlist.insert( published, &path ) } } } if playlist.write_as(folders::MASTER_PLAYLIST_PATH, true)? { println!("[info] generated master playlist"); } Ok(()) } pub(crate) fn generate_podcast_m3u( alias: &str, root: &path::Path, unlistened_only: bool, ) -> anyhow::Result<()> { let mut playlist = Playlist::new(root); let output = folders::podcast_folder(root, alias); let spec_file = output.join(folders::SPEC_FILE); let spec = Specification::read_from(&spec_file)?; let episodes = spec .feed_iter() .map(|(published, eps)| iter::repeat(published).zip(eps.iter()) ) .flatten() .filter(|episode| !unlistened_only || (unlistened_only && !episode.1.listened) ); for (published, episode) in episodes { let path = output.join( spec.path_from_id(episode.id()).unwrap() ); playlist.insert( published.clone(), &path, ); } let written = if unlistened_only { playlist.write_as(&format!("[PC] {} (unlistened)", alias), false) } else { playlist.write_as(&format!("[PC] {}", alias), false) }?; if written { println!("[info] generated playlist for podcast {:?}.", alias); } Ok(()) }