From 5291730793071c30e215a75102bd699472d15564 Mon Sep 17 00:00:00 2001 From: Aaron Manning Date: Mon, 1 Sep 2025 10:07:28 +1000 Subject: [PATCH] reorganise modules --- src/download.rs | 136 ++++++------------------------ src/input.rs | 6 -- src/main.rs | 22 ++--- src/manage.rs | 142 ++++++++++++++++++++++++++++++++ src/{tagging.rs => playlist.rs} | 45 ++-------- 5 files changed, 181 insertions(+), 170 deletions(-) create mode 100644 src/manage.rs rename src/{tagging.rs => playlist.rs} (73%) diff --git a/src/download.rs b/src/download.rs index b096316..ea15150 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,92 +1,21 @@ -use std::fs; -use std::path; -use std::borrow::Cow; -use std::iter::Iterator; -use std::collections::{HashMap, BTreeMap, HashSet}; +use std::{ + fs, + path, + borrow::Cow, + collections::HashSet, +}; use anyhow::Context; use sanitise_file_name::sanitise; -use crate::folders; -use crate::rss; - - -#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] -pub(crate) struct Specification<'a> { - files: HashMap, Cow<'a, path::Path>>, - /// This is a collection of episodes, where each entry contains a `Vec` of - /// episodes to allow for the possibility that multiple episodes have the - /// same timestamp. - feed: BTreeMap>>, - image_url: Option>, -} - -impl<'a> Specification<'a> { - 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 { - Specification::default() - }) - } - - 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() - } - - pub(crate) fn path_from_id(&self, id: &str) -> Option<&path::Path> { - self.files.get(id).map(|v| &**v) - } - - pub(crate) fn feed(&self) -> &BTreeMap>> { - &self.feed - } - - pub(crate) fn into_feed_and_files(self) -> (BTreeMap>>, HashMap, Cow<'a, path::Path>>) { - (self.feed, self.files) - } -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub (crate) struct Episode<'a> { - /// Episode title. - title: Cow<'a, str>, - /// Show notes pulled from description or summary tag. - show_notes: Option>, - /// This is the GUID or the URL if the GUID is not present. - 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() - } - - pub(crate) fn id(&self) -> &str { - &self.id - } -} +use crate::{ + rss, + folders, + manage::{ + Specification, + Episode, + }, +}; fn download_to_file(url: &str, path: &path::Path) -> anyhow::Result<()> { let response = minreq::get(url) @@ -169,7 +98,7 @@ fn update_artwork<'a, 'b>( _ => None, }; - match (&spec.image_url, image_url) { + match (spec.image_url.as_deref(), image_url) { // They match, so no need to change anything (Some(old), Some(new)) if old == new => (), // New and different URL @@ -268,7 +197,7 @@ pub(crate) fn update_podcast_from_feed( let id = guid.unwrap_or(url); - match spec.files.get(id) { + match spec.path_from_id(id) { // File already downloaded Some(path) => { // File has been deleted by another process but the specification hasn't been updated @@ -315,30 +244,15 @@ pub(crate) fn update_podcast_from_feed( Ok(()) => { let file_path = file_path.canonicalize().unwrap(); - spec.files.insert( - Cow::from(id.to_owned()), - Cow::from(file_path.strip_prefix(&output).unwrap().to_owned()), - ); + spec.insert_into_files( + id.to_owned(), + file_path.strip_prefix(&output).unwrap().to_owned(), + )?; - let episode = Episode { - show_notes: description, - id: Cow::from(id.to_owned()), - current: true, - title, - listened: false, - }; + let episode = Episode::new_downloaded(title, description, id.to_owned()); - match spec.feed.get_mut(&item.published) { - Some(existing) => { - existing.push(episode) - }, - None => { - spec.feed.insert( - item.published, - vec![episode], - ); - } - } + + spec.insert_into_feed(item.published, episode); // Update the file as we go, but only if a change has occured spec.write_to(&spec_file)?; @@ -354,9 +268,9 @@ pub(crate) fn update_podcast_from_feed( let mut feed_change = false; // Setting episodes which have been removed to no longer be current - for (_, episodes) in &mut spec.feed { + for (_, episodes) in spec.feed_iter_mut() { for episode in episodes { - if !current_episodes.contains(episode.id.as_ref()) { + if !current_episodes.contains(episode.id()) { episode.current = false; feed_change = true; } diff --git a/src/input.rs b/src/input.rs index 04663d8..677ef3a 100644 --- a/src/input.rs +++ b/src/input.rs @@ -42,12 +42,6 @@ pub(crate) enum Command { #[arg(long, short)] podcast: String, }, - /// Tags files for use with an iPod, such that they don't show up in albums and artists. - Tag { - /// The podcast to tag. - #[arg(long, short)] - podcast: Option, - }, /// Generates playlist for use with an iPod. Playlist { /// The podcast to generate the playlist for. diff --git a/src/main.rs b/src/main.rs index 287b7ce..2134ea0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ mod rss; mod input; -mod tagging; +mod manage; mod folders; +mod playlist; mod download; use input::{Command, ListenStatus}; @@ -51,7 +52,7 @@ fn main() -> anyhow::Result<()> { let output = folders::podcast_folder(root, &podcast); let spec_file = output.join("spec.toml"); - let spec = download::Specification::read_from(&spec_file)?; + let spec = manage::Specification::read_from(&spec_file)?; for (_, episodes) in spec.feed_iter() { for episode in episodes { @@ -67,7 +68,7 @@ fn main() -> anyhow::Result<()> { let output = folders::podcast_folder(root, &podcast); let spec_file = output.join("spec.toml"); - let mut spec = download::Specification::read_from(&spec_file)?; + let mut spec = manage::Specification::read_from(&spec_file)?; for (_, episodes) in spec.feed_iter_mut() { for episode in episodes { @@ -79,21 +80,12 @@ fn main() -> anyhow::Result<()> { }, Command::Playlist { podcast } => { if let Some(alias) = podcast { - tagging::generate_podcast_m3u(alias.as_str(), root)?; + playlist::generate_podcast_m3u(alias.as_str(), root)?; } else { for (alias, _) in &config.podcasts { - tagging::generate_podcast_m3u(alias.as_str(), root)?; - } - tagging::generate_master_m3u(&config, root)?; - } - }, - Command::Tag { podcast } => { - if let Some(alias) = podcast { - tagging::strip_tags(alias.as_str(), root)?; - } else { - for (alias, _) in config.podcasts { - tagging::strip_tags(alias.as_str(), root)?; + playlist::generate_podcast_m3u(alias.as_str(), root)?; } + playlist::generate_master_m3u(&config, root)?; } }, }; diff --git a/src/manage.rs b/src/manage.rs new file mode 100644 index 0000000..7c377b5 --- /dev/null +++ b/src/manage.rs @@ -0,0 +1,142 @@ +use std::{ + fs, + path, + borrow::Cow, + collections::{ + BTreeMap, + HashMap, + }, +}; + +use anyhow::Context; + +#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] +pub(crate) struct Specification<'a> { + files: HashMap, Cow<'a, path::Path>>, + /// This is a collection of episodes, where each entry contains a `Vec` of + /// episodes to allow for the possibility that multiple episodes have the + /// same timestamp. + feed: BTreeMap>>, + pub(in crate) image_url: Option>, +} + +impl<'a> Specification<'a> { + /// Reads from the specification file from a given path, or gives a default + /// if the file doesn't exist. + 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 { + Specification::default() + }) + } + + /// Reads from the specification file from a given path. + 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") + }) + } + + /// Writes the specification to the specific file path. + 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() + } + + pub(crate) fn path_from_id(&self, id: &str) -> Option<&path::Path> { + self.files.get(id).map(|v| &**v) + } + + pub(crate) fn into_feed_and_files( + self + ) -> ( + BTreeMap>>, + HashMap, Cow<'a, path::Path>>, + ) { + (self.feed, self.files) + } + + pub(crate) fn insert_into_files( + &mut self, + id: impl Into>, + path: impl Into>, + ) -> anyhow::Result<()> { + self.files.insert( + id.into(), + path.into() + ) + .context("insertion of episode with duplicate id") + .map(|_| ()) + } + + pub(crate) fn insert_into_feed( + &mut self, + published: chrono::NaiveDateTime, + episode: Episode<'a> + ) { + match self.feed.get_mut(&published) { + Some(existing) => { + existing.push(episode) + }, + None => { + self.feed.insert( + published, + vec![episode], + ); + } + } + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub (crate) struct Episode<'a> { + /// Episode title. + title: Cow<'a, str>, + /// Show notes pulled from description or summary tag. + show_notes: Option>, + /// This is the GUID or the URL if the GUID is not present. + id: Cow<'a, str>, + /// If the episode exists in the latest version of the feed. + pub(crate) 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 new_downloaded( + title: impl Into>, + show_notes: Option>, + id: impl Into>, + ) -> Self { + Self { + title: title.into(), + show_notes, + id: id.into(), + current: true, + listened: false, + } + } + + pub(crate) fn title(&self) -> &str { + self.title.as_ref() + } + + pub(crate) fn id(&self) -> &str { + &self.id + } +} diff --git a/src/tagging.rs b/src/playlist.rs similarity index 73% rename from src/tagging.rs rename to src/playlist.rs index ecdfc6f..7160d00 100644 --- a/src/tagging.rs +++ b/src/playlist.rs @@ -1,6 +1,10 @@ use std::{fs, path, collections::BTreeMap}; -use crate::{input, folders, download}; +use crate::{ + input, + folders, + manage::Specification, +}; use anyhow::Context; @@ -15,7 +19,7 @@ pub(crate) fn generate_master_m3u( for (podcast, _) in &config.podcasts { let output = folders::podcast_folder(root, podcast.as_str()); let spec_file = output.join("spec.toml"); - let spec = download::Specification::read_from(&spec_file)?; + let spec = Specification::read_from(&spec_file)?; let (feed, files) = spec.into_feed_and_files(); @@ -72,7 +76,7 @@ pub(crate) fn generate_podcast_m3u( let output = folders::podcast_folder(root, alias); let spec_file = output.join("spec.toml"); - let spec = download::Specification::read_from(&spec_file)?; + let spec = Specification::read_from(&spec_file)?; let mut playlist = Vec::new(); @@ -108,38 +112,3 @@ pub(crate) fn generate_podcast_m3u( Ok(()) } - -pub(crate) fn strip_tags( - alias: &str, - root: &path::Path, -) -> anyhow::Result<()> { - println!("[info] retagging podcast {}", alias); - - let output = folders::podcast_folder(root, alias); - let spec_file = output.join("spec.toml"); - - let spec = download::Specification::read_from(&spec_file)?; - - for episode in spec.feed_iter().map(|(_, eps)| eps.iter()).flatten() { - let path = output.join( - spec.path_from_id(episode.id()).unwrap() - ); - let file = audiotags::Tag::new().read_from_path( - &path - ); - - let Ok(mut file) = file else { - println!("[warning] failed to read mp3 audio tags file. skipping."); - continue - }; - - file.remove_artist(); - file.remove_album(); - file.set_genre("Podcast"); - file.set_title(episode.title()); - - file.write_to_path(path.as_path().to_str().unwrap())?; - } - - Ok(()) -}