use std::{ fs, 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(), } } fn write_as( &self, name: &str, reverse: bool, ) -> anyhow::Result<()> { let playlists_folder = self.root.join("Playlists"); 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 file = fs::File::create(path)?; let mut writer = m3u::Writer::new(&mut file); 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)?; } }; Ok(()) } 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::PODCASTS_DIR) ).unwrap(); path::Path::new("/Podcasts").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("spec.toml"); 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 ) } } } playlist.write_as("[Podcast Master Feed]", true) } 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("spec.toml"); 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, ); } if unlistened_only { playlist.write_as(&format!("{} (unlistened)", alias), false) } else { playlist.write_as(alias, false) } }