diff --git a/src/download.rs b/src/download.rs index ff1f784..8a672fa 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,6 +1,7 @@ use std::fs; use std::path; use std::borrow::Cow; +use std::iter::Iterator; use std::collections::{HashMap, BTreeMap, HashSet}; use anyhow::Context; @@ -9,14 +10,14 @@ use sanitise_file_name::sanitise; use crate::rss; #[derive(Default, serde::Serialize, serde::Deserialize)] -struct Specification<'a> { +pub (crate) struct Specification<'a> { files : HashMap, Cow<'a, path::Path>>, feed : BTreeMap>>, image_url : Option>, } impl<'a> Specification<'a> { - fn read_from(path : &path::Path) -> Result { + pub (crate) fn read_from_with_default(path : &path::Path) -> Result { Ok(if path.is_file() { toml::from_str(&fs::read_to_string(&path)?[..])? } else { @@ -24,13 +25,29 @@ impl<'a> Specification<'a> { }) } - fn write_to(&self, path : &path::Path) -> Result<(), anyhow::Error> { + pub (crate) fn read_from(path : &path::Path) -> Result { + Ok(if path.is_file() { + toml::from_str(&fs::read_to_string(&path)?[..])? + } else { + anyhow::bail!("could not find specification for the desired podcast") + }) + } + + pub (crate) fn write_to(&self, path : &path::Path) -> Result<(), anyhow::Error> { Ok(fs::write(path, toml::to_string(self)?.as_bytes())?) } + + pub (crate) fn feed_iter(&self) -> impl Iterator>)> { + self.feed.iter() + } + + pub (crate) fn feed_iter_mut(&mut self) -> impl Iterator>)> { + self.feed.iter_mut() + } } #[derive(serde::Serialize, serde::Deserialize)] -struct Episode<'a> { +pub (crate) struct Episode<'a> { /// Episode title. title : Cow<'a, str>, /// Show notes pulled from description or summary tag. @@ -39,6 +56,15 @@ struct Episode<'a> { id : Cow<'a, str>, /// If the episode exists in the latest version of the feed. current : bool, + /// Flag to keep track of which episodes have been listened to. + #[serde(default)] + pub (crate) listened : bool, +} + +impl<'a> Episode<'a> { + pub (crate) fn title(&self) -> &str { + self.title.as_ref() + } } fn download_to_file(url : &str, path : &path::Path) -> anyhow::Result<()> { @@ -175,7 +201,7 @@ pub (crate) fn update_podcast_from_feed( let spec_file = output.join("spec.toml"); - let mut spec = Specification::read_from(&spec_file)?; + let mut spec = Specification::read_from_with_default(&spec_file)?; // Get set of all currently available episodes such that we can later mark // any other episodes as unavailable @@ -278,6 +304,7 @@ pub (crate) fn update_podcast_from_feed( id : Cow::from(id.to_owned()), current : true, title, + listened : false, }; match spec.feed.get_mut(&item.published) { diff --git a/src/input.rs b/src/input.rs index 7faa813..c9818ad 100644 --- a/src/input.rs +++ b/src/input.rs @@ -6,9 +6,42 @@ pub (crate) struct Args { /// Path to the configuration file listing podcast RSS feeds. #[arg(default_value = "./podcasts.toml")] pub (crate) config : path::PathBuf, - /// The podcast to update. Updates all in configuration file if unspecified. - #[arg(long, short)] - pub (crate) podcast : Option, + #[command(subcommand)] + pub (crate) command : Command, +} + +#[derive(clap::ValueEnum, Copy, Clone, Debug,PartialEq, Eq, PartialOrd, Ord)] +pub (crate) enum ListenStatus { + Listened, + Unlistened, +} + +#[derive(clap::Subcommand)] +pub (crate) enum Command { + /// Updates feed and downloads latest episodes. + Download { + /// The podcast to update. Updates all in configuration file if unspecified. + #[arg(long, short)] + podcast : Option, + }, + /// Lists the episodes for a given podcast, filtered based on if it has been listened or not. + List { + /// Filter for if episodes have been listened to or not. + #[arg(long, short)] + status : Option, + /// The podcast to list episodes for. + #[arg(long, short)] + podcast : String, + }, + /// Marks an entire podcast's history of episodes as played or unplayed. + Mark { + /// The new listen status for the episodes. + #[arg(long, short)] + status : ListenStatus, + /// The podcast to change the listen status of. + #[arg(long, short)] + podcast : String, + }, } /// Struct modelling configuration file format. diff --git a/src/main.rs b/src/main.rs index 3e574aa..57c6454 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,12 @@ mod rss; mod input; mod download; +use input::{Command, ListenStatus}; + use std::fs; use anyhow::Context; +use sanitise_file_name::sanitise; fn main() -> anyhow::Result<()> { @@ -25,23 +28,59 @@ fn main() -> anyhow::Result<()> { anyhow::bail!("could not get parent of configuration path for root directory") }; - // Updating single podcast - if let Some(alias) = args.podcast { - if let Some(feed_url) = config.podcasts.get(&alias) { - download::update_podcast(&alias, root, feed_url)?; - } - else { - anyhow::bail!(r#"podcast "{}" not found in configuration file"#, alias) - } - } - // Updating all podcasts - else { - for (alias, feed_url) in config.podcasts { - download::update_podcast(&alias, root, &feed_url)?; - } - } + + match args.command { + Command::Download { podcast } => { + // Updating single podcast + if let Some(alias) = podcast { + if let Some(feed_url) = config.podcasts.get(&alias) { + download::update_podcast(&alias, root, feed_url)?; + } + else { + anyhow::bail!(r#"podcast "{}" not found in configuration file"#, alias) + } + } + // Updating all podcasts + else { + for (alias, feed_url) in config.podcasts { + download::update_podcast(&alias, root, &feed_url)?; + } + } + }, + Command::List { status, podcast } => { + let output = root.join(sanitise(&podcast)); + let spec_file = output.join("spec.toml"); + + let spec = download::Specification::read_from(&spec_file)?; + + for (_, episodes) in spec.feed_iter() { + for episode in episodes { + if status.is_none() + || (episode.listened && status.is_some_and(|x| x == ListenStatus::Listened)) + || (!episode.listened && status.is_some_and(|x| x == ListenStatus::Unlistened)) { + println!("{}", episode.title()) + } + } + } + }, + Command::Mark { status, podcast } => { + let output = root.join(sanitise(&podcast)); + let spec_file = output.join("spec.toml"); + + let mut spec = download::Specification::read_from(&spec_file)?; + + for (_, episodes) in spec.feed_iter_mut() { + for episode in episodes { + episode.listened = status == ListenStatus::Listened; + } + } + + spec.write_to(&spec_file)?; + }, + }; Ok(()) } +