separate playlist generation

This commit is contained in:
Aaron Manning
2025-09-01 09:42:15 +10:00
parent 07b43ba2dc
commit 2b55c5b0af
4 changed files with 98 additions and 12 deletions

View File

@@ -53,6 +53,14 @@ impl<'a> Specification<'a> {
pub(crate) fn path_from_id(&self, id: &str) -> Option<&path::Path> { pub(crate) fn path_from_id(&self, id: &str) -> Option<&path::Path> {
self.files.get(id).map(|v| &**v) self.files.get(id).map(|v| &**v)
} }
pub(crate) fn feed(&self) -> &BTreeMap<chrono::NaiveDateTime, Vec<Episode<'a>>> {
&self.feed
}
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)
}
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]

View File

@@ -42,9 +42,15 @@ pub(crate) enum Command {
#[arg(long, short)] #[arg(long, short)]
podcast: String, 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 { Tag {
/// The podcast to tag and generate playlists for. /// The podcast to tag.
#[arg(long, short)]
podcast: Option<String>,
},
/// Generates playlist for use with an iPod.
Playlist {
/// The podcast to generate the playlist for.
#[arg(long, short)] #[arg(long, short)]
podcast: Option<String>, podcast: Option<String>,
}, },

View File

@@ -77,17 +77,25 @@ fn main() -> anyhow::Result<()> {
spec.write_to(&spec_file)?; 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 } => { Command::Tag { podcast } => {
if let Some(alias) = podcast { if let Some(alias) = podcast {
tagging::generate_m3u(alias.as_str(), root)?;
tagging::strip_tags(alias.as_str(), root)?; tagging::strip_tags(alias.as_str(), root)?;
} else { } else {
for (alias, _) in config.podcasts { for (alias, _) in config.podcasts {
tagging::generate_m3u(alias.as_str(), root)?;
tagging::strip_tags(alias.as_str(), root)?; tagging::strip_tags(alias.as_str(), root)?;
} }
} }
} },
}; };
Ok(()) Ok(())

View File

@@ -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 anyhow::Context;
use sanitise_file_name::sanitise; 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::<chrono::NaiveDateTime, Vec<_>>::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, alias: &str,
root: &path::Path, root: &path::Path,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@@ -54,6 +113,8 @@ pub(crate) fn strip_tags(
alias: &str, alias: &str,
root: &path::Path, root: &path::Path,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
println!("[info] retagging podcast {}", alias);
let output = folders::podcast_folder(root, alias); let output = folders::podcast_folder(root, alias);
let spec_file = output.join("spec.toml"); let spec_file = output.join("spec.toml");
@@ -63,13 +124,16 @@ pub(crate) fn strip_tags(
let path = output.join( let path = output.join(
spec.path_from_id(episode.id()).unwrap() 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 &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_artist();
file.remove_year();
file.remove_album(); file.remove_album();
file.set_genre("Podcast"); file.set_genre("Podcast");
file.set_title(episode.title()); file.set_title(episode.title());