From fcc6acda5632816c613845ba84f04380dd97ad62 Mon Sep 17 00:00:00 2001 From: Aaron Manning Date: Tue, 2 Sep 2025 11:04:27 +1000 Subject: [PATCH] unlistened feeds --- src/main.rs | 6 +- src/playlist.rs | 173 ++++++++++++++++++++++++++++++------------------ 2 files changed, 114 insertions(+), 65 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2134ea0..9a26272 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,10 +80,12 @@ fn main() -> anyhow::Result<()> { }, Command::Playlist { podcast } => { if let Some(alias) = podcast { - playlist::generate_podcast_m3u(alias.as_str(), root)?; + playlist::generate_podcast_m3u(alias.as_str(), root, false)?; + playlist::generate_podcast_m3u(alias.as_str(), root, true)?; } else { for (alias, _) in &config.podcasts { - playlist::generate_podcast_m3u(alias.as_str(), root)?; + playlist::generate_podcast_m3u(alias.as_str(), root, true)?; + playlist::generate_podcast_m3u(alias.as_str(), root, false)?; } playlist::generate_master_m3u(&config, root)?; } diff --git a/src/playlist.rs b/src/playlist.rs index 7160d00..ec840e8 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -1,4 +1,9 @@ -use std::{fs, path, collections::BTreeMap}; +use std::{ + fs, + iter, + path, + collections::BTreeMap, +}; use crate::{ input, @@ -10,11 +15,86 @@ use anyhow::Context; use sanitise_file_name::sanitise; +struct Playlist<'a> { + root: &'a path::Path, + files: BTreeMap>, +} + +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 all_episodes = BTreeMap::>::new(); + let mut playlist = Playlist::new(root); for (podcast, _) in &config.podcasts { let output = folders::podcast_folder(root, podcast.as_str()); @@ -28,87 +108,54 @@ pub(crate) fn generate_master_m3u( let path = output.join( files.get(episode.id()).unwrap() ); - let entry = m3u::path_entry({ - let relative = path.strip_prefix( - output.parent().unwrap() - ).unwrap(); - path::Path::new("/Podcasts").join(relative) - }); - - match all_episodes.get_mut(&published) { - Some(existing) => { - existing.push(entry) - }, - None => { - all_episodes.insert( - published, - vec![entry] - ); - } - } + + playlist.insert( + published, + &path + ) } } } - let playlists_folder = 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("_master"); - path.set_extension("m3u"); - - let mut file = fs::File::create(path)?; - - let mut writer = m3u::Writer::new(&mut file); - for entry in all_episodes.values().map(|entries| entries.iter()).flatten() { - writer.write_entry(entry)?; - } - - Ok(()) + 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 mut playlist = Vec::new(); + 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 episode in spec.feed_iter().map(|(_, eps)| eps.iter()).flatten() { + for (published, episode) in episodes { let path = output.join( spec.path_from_id(episode.id()).unwrap() ); - playlist.push(m3u::path_entry({ - let relative = path.strip_prefix( - output.parent().unwrap() - ).unwrap(); - path::Path::new("/Podcasts").join(relative) - })); + playlist.insert( + published.clone(), + &path, + ); } - // Write the playlist file - { - let playlists_folder = 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(&alias)); - path.set_extension("m3u"); - - let mut file = fs::File::create(path)?; - - let mut writer = m3u::Writer::new(&mut file); - for entry in &playlist { - writer.write_entry(entry)?; - } + if unlistened_only { + playlist.write_as(&format!("{} (unlistened)", alias), false) + } else { + playlist.write_as(alias, false) } - - Ok(()) }