initial code for tag stripping and m3u generation

This commit is contained in:
Aaron Manning
2025-08-29 21:33:20 +10:00
parent 105a3eb892
commit cb47ff0cb8
6 changed files with 337 additions and 49 deletions

View File

@@ -9,15 +9,18 @@ use sanitise_file_name::sanitise;
use crate::rss;
#[derive(Default, serde::Serialize, serde::Deserialize)]
pub (crate) struct Specification<'a> {
files : HashMap<Cow<'a, str>, Cow<'a, path::Path>>,
feed : BTreeMap<chrono::NaiveDateTime, Vec<Episode<'a>>>,
image_url : Option<Cow<'a, str>>,
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct Specification<'a> {
files: HashMap<Cow<'a, str>, 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<chrono::NaiveDateTime, Vec<Episode<'a>>>,
image_url: Option<Cow<'a, str>>,
}
impl<'a> Specification<'a> {
pub (crate) fn read_from_with_default(path : &path::Path) -> Result<Self, anyhow::Error> {
pub(crate) fn read_from_with_default(path: &path::Path) -> Result<Self, anyhow::Error> {
Ok(if path.is_file() {
toml::from_str(&fs::read_to_string(&path)?[..])?
} else {
@@ -25,7 +28,7 @@ impl<'a> Specification<'a> {
})
}
pub (crate) fn read_from(path : &path::Path) -> Result<Self, anyhow::Error> {
pub(crate) fn read_from(path: &path::Path) -> Result<Self, anyhow::Error> {
Ok(if path.is_file() {
toml::from_str(&fs::read_to_string(&path)?[..])?
} else {
@@ -33,41 +36,49 @@ impl<'a> Specification<'a> {
})
}
pub (crate) fn write_to(&self, path : &path::Path) -> Result<(), anyhow::Error> {
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<Item = (&chrono::NaiveDateTime, &Vec<Episode<'a>>)> {
pub(crate) fn feed_iter(&self) -> impl Iterator<Item = (&chrono::NaiveDateTime, &Vec<Episode<'a>>)> {
self.feed.iter()
}
pub (crate) fn feed_iter_mut(&mut self) -> impl Iterator<Item = (&chrono::NaiveDateTime, &mut Vec<Episode<'a>>)> {
pub(crate) fn feed_iter_mut(&mut self) -> impl Iterator<Item = (&chrono::NaiveDateTime, &mut Vec<Episode<'a>>)> {
self.feed.iter_mut()
}
pub(crate) fn path_from_id(&self, id: &str) -> Option<&path::Path> {
self.files.get(id).map(|v| &**v)
}
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub (crate) struct Episode<'a> {
/// Episode title.
title : Cow<'a, str>,
title: Cow<'a, str>,
/// Show notes pulled from description or summary tag.
show_notes : Option<Cow<'a, str>>,
show_notes: Option<Cow<'a, str>>,
/// This is the GUID or the URL if the GUID is not present.
id : Cow<'a, str>,
id: Cow<'a, str>,
/// If the episode exists in the latest version of the feed.
current : bool,
current: bool,
/// Flag to keep track of which episodes have been listened to.
#[serde(default)]
pub (crate) listened : bool,
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
}
}
fn download_to_file(url : &str, path : &path::Path) -> anyhow::Result<()> {
fn download_to_file(url: &str, path: &path::Path) -> anyhow::Result<()> {
let response = minreq::get(url)
.send()?;
@@ -80,10 +91,10 @@ fn download_to_file(url : &str, path : &path::Path) -> anyhow::Result<()> {
Ok(())
}
pub (crate) fn update_podcast(
alias : &str,
root : &path::Path,
feed_location : &str,
pub(crate) fn update_podcast(
alias: &str,
root: &path::Path,
feed_location: &str,
) -> anyhow::Result<()> {
// Create output directory
@@ -126,7 +137,7 @@ pub (crate) fn update_podcast(
}
}
fn extract_extension_from_url(url : &str) -> Result<Option<String>, url::ParseError> {
fn extract_extension_from_url(url: &str) -> Result<Option<String>, url::ParseError> {
let mut url_edited = url::Url::parse(url)?;
url_edited.set_query(None);
@@ -137,10 +148,10 @@ fn extract_extension_from_url(url : &str) -> Result<Option<String>, url::ParseEr
}
fn update_artwork<'a, 'b>(
channel : &rss::Channel<'a>,
spec : &mut Specification<'b>,
output : &path::Path,
) -> anyhow::Result<()> where 'a : 'b {
channel: &rss::Channel<'a>,
spec: &mut Specification<'b>,
output: &path::Path,
) -> anyhow::Result<()> where 'a: 'b {
let image_url = match (&channel.image, &channel.itunes_image) {
(Some(image), _) => Some(&image.url),
@@ -156,7 +167,7 @@ fn update_artwork<'a, 'b>(
match extract_extension_from_url(new.as_ref()) {
Ok(Some(extension)) => {
let cover_path = output.join(format!("cover.{}", extension));
let cover_path = output.join(format!("cover-original.{}", extension));
// Remove cover with conflicting file path if it exists
if cover_path.exists() {
@@ -184,9 +195,9 @@ fn update_artwork<'a, 'b>(
}
pub (crate) fn update_podcast_from_feed(
output : &path::Path,
feed : &str,
pub(crate) fn update_podcast_from_feed(
output: &path::Path,
feed: &str,
) -> anyhow::Result<()> {
let feed = match xml_serde::from_str::<rss::Feed>(&feed) {
@@ -300,11 +311,11 @@ pub (crate) fn update_podcast_from_feed(
);
let episode = Episode {
show_notes : description,
id : Cow::from(id.to_owned()),
current : true,
show_notes: description,
id: Cow::from(id.to_owned()),
current: true,
title,
listened : false,
listened: false,
};
match spec.feed.get_mut(&item.published) {
@@ -352,12 +363,12 @@ pub (crate) fn update_podcast_from_feed(
/// Given a file path `something.xyz`, returns the first path of the form
/// `something(a).xyz` where `a` is a non-negative integer which does not
/// currently exist, or `something.xyz` if it itself does not exist.
fn increment_file_name(path : &path::Path) -> Cow<'_, path::Path> {
fn increment_file_name(path: &path::Path) -> Cow<'_, path::Path> {
if path.exists() {
let mut new_path = path.to_owned();
let mut i : u32 = 0;
let mut i: u32 = 0;
while new_path.exists() {
let mut stem = path.file_stem().unwrap().to_owned();