From 2b55c5b0af6b1c17334dcf27c0d967cdcc6ad894 Mon Sep 17 00:00:00 2001 From: Aaron Manning Date: Mon, 1 Sep 2025 09:42:15 +1000 Subject: [PATCH] separate playlist generation --- src/download.rs | 8 +++++ src/input.rs | 10 +++++-- src/main.rs | 14 +++++++-- src/tagging.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 98 insertions(+), 12 deletions(-) diff --git a/src/download.rs b/src/download.rs index 10dd1af..b096316 100644 --- a/src/download.rs +++ b/src/download.rs @@ -53,6 +53,14 @@ impl<'a> Specification<'a> { 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)] diff --git a/src/input.rs b/src/input.rs index 3ffbb6b..04663d8 100644 --- a/src/input.rs +++ b/src/input.rs @@ -42,9 +42,15 @@ pub(crate) enum Command { #[arg(long, short)] podcast: String, }, - /// Tags files and generates playlists ready for use with an iPod. + /// Tags files for use with an iPod, such that they don't show up in albums and artists. Tag { - /// The podcast to tag and generate playlists for. + /// The podcast to tag. + #[arg(long, short)] + podcast: Option, + }, + /// Generates playlist for use with an iPod. + Playlist { + /// The podcast to generate the playlist for. #[arg(long, short)] podcast: Option, }, diff --git a/src/main.rs b/src/main.rs index 7d72d11..287b7ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,17 +77,25 @@ fn main() -> anyhow::Result<()> { spec.write_to(&spec_file)?; }, + Command::Playlist { podcast } => { + if let Some(alias) = podcast { + tagging::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::generate_m3u(alias.as_str(), root)?; tagging::strip_tags(alias.as_str(), root)?; } else { for (alias, _) in config.podcasts { - tagging::generate_m3u(alias.as_str(), root)?; tagging::strip_tags(alias.as_str(), root)?; } } - } + }, }; Ok(()) diff --git a/src/tagging.rs b/src/tagging.rs index 38f0ccc..ecdfc6f 100644 --- a/src/tagging.rs +++ b/src/tagging.rs @@ -1,12 +1,71 @@ -use std::{fs, path}; +use std::{fs, path, collections::BTreeMap}; -use crate::{folders, download}; +use crate::{input, folders, download}; use anyhow::Context; use sanitise_file_name::sanitise; -pub(crate) fn generate_m3u( +pub(crate) fn generate_master_m3u( + config: &input::Config, + root: &path::Path, +) -> anyhow::Result<()> { + let mut all_episodes = BTreeMap::>::new(); + + 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 (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() + ); + 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] + ); + } + } + } + } + } + + 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(()) +} + + +pub(crate) fn generate_podcast_m3u( alias: &str, root: &path::Path, ) -> anyhow::Result<()> { @@ -54,6 +113,8 @@ 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"); @@ -63,13 +124,16 @@ pub(crate) fn strip_tags( let path = output.join( spec.path_from_id(episode.id()).unwrap() ); - let mut file = audiotags::Tag::new().read_from_path( + 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_title(); file.remove_artist(); - file.remove_year(); file.remove_album(); file.set_genre("Podcast"); file.set_title(episode.title());