2025-09-01 10:07:28 +10:00
|
|
|
use std::{
|
|
|
|
|
fs,
|
|
|
|
|
path,
|
|
|
|
|
borrow::Cow,
|
|
|
|
|
collections::{
|
|
|
|
|
BTreeMap,
|
|
|
|
|
HashMap,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[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>>>,
|
|
|
|
|
pub(in crate) image_url: Option<Cow<'a, str>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<Self, anyhow::Error> {
|
|
|
|
|
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<Self, anyhow::Error> {
|
|
|
|
|
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<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>>)> {
|
|
|
|
|
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<chrono::NaiveDateTime, Vec<Episode<'a>>>,
|
|
|
|
|
HashMap<Cow<'a, str>, Cow<'a, path::Path>>,
|
|
|
|
|
) {
|
|
|
|
|
(self.feed, self.files)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn insert_into_files(
|
|
|
|
|
&mut self,
|
|
|
|
|
id: impl Into<Cow<'a, str>>,
|
|
|
|
|
path: impl Into<Cow<'a, path::Path>>,
|
2025-09-21 09:07:18 +10:00
|
|
|
) -> Option<Cow<'a, path::Path>> {
|
2025-09-01 10:07:28 +10:00
|
|
|
self.files.insert(
|
|
|
|
|
id.into(),
|
|
|
|
|
path.into()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-21 09:07:18 +10:00
|
|
|
|
|
|
|
|
pub(crate) fn files_and_feed_mut(
|
|
|
|
|
&mut self,
|
|
|
|
|
) -> (
|
|
|
|
|
&mut BTreeMap<chrono::NaiveDateTime, Vec<Episode<'a>>>,
|
|
|
|
|
&HashMap<Cow<'a, str>, Cow<'a, path::Path>>,
|
|
|
|
|
) {
|
|
|
|
|
(&mut self.feed, &self.files)
|
|
|
|
|
}
|
2025-09-01 10:07:28 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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<Cow<'a, str>>,
|
|
|
|
|
/// 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<Cow<'a, str>>,
|
|
|
|
|
show_notes: Option<Cow<'a, str>>,
|
|
|
|
|
id: impl Into<Cow<'a, str>>,
|
|
|
|
|
) -> 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
|
|
|
|
|
}
|
|
|
|
|
}
|