Files
podcast-hoarder/src/playlist.rs
2025-09-02 11:04:27 +10:00

162 lines
3.8 KiB
Rust

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<chrono::NaiveDateTime, Vec<m3u::Entry>>,
}
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)
}
}