initial code for tag stripping and m3u generation

This commit is contained in:
Aaron Manning
2025-08-29 21:33:20 +10:00
parent 105a3eb892
commit cb47ff0cb8
6 changed files with 337 additions and 49 deletions

198
Cargo.lock generated
View File

@@ -1,6 +1,12 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
@@ -91,18 +97,50 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "audiotags"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44e797ce0164cf599c71f2c3849b56301d96a3dc033544588e875686b050ed39"
dependencies = [
"audiotags-macro",
"id3",
"metaflac",
"mp4ameta",
"readme-rustdocifier",
"thiserror",
]
[[package]]
name = "audiotags-macro"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaa9b2312fc01f7291f3b7b0f52ed08b1c0177c96a2e696ab55695cc4d06889"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.14.0" version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.83" version = "1.0.83"
@@ -185,6 +223,15 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.9.0" version = "1.9.0"
@@ -210,13 +257,23 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "flate2"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [ dependencies = [
"percent-encoding", "percent-encoding 2.3.1",
] ]
[[package]] [[package]]
@@ -289,6 +346,28 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "id3"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aadb14a5ba1a0d58ecd4a29bfc9b8f1d119eee24aa01a62c1ec93eb9630a1d86"
dependencies = [
"bitflags",
"byteorder",
"flate2",
]
[[package]]
name = "idna"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@@ -327,6 +406,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.152" version = "0.2.152"
@@ -339,12 +424,46 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "m3u"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca48bebf6a7397a81aa8b9dcc8a7cfcbacab3e15a28f49d2aabaa9e3c06def4d"
dependencies = [
"url 1.7.2",
]
[[package]]
name = "matches"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.1" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "metaflac"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdf25a3451319c52a4a56d956475fbbb763bfb8420e2187d802485cb0fd8d965"
dependencies = [
"byteorder",
"hex",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]] [[package]]
name = "minreq" name = "minreq"
version = "2.11.0" version = "2.11.0"
@@ -358,6 +477,22 @@ dependencies = [
"webpki-roots", "webpki-roots",
] ]
[[package]]
name = "mp4ameta"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb23d62e8eb5299a3f79657c70ea9269eac8f6239a76952689bcd06a74057e81"
dependencies = [
"lazy_static",
"mp4ameta_proc",
]
[[package]]
name = "mp4ameta_proc"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07dcca13d1740c0a665f77104803360da0bdb3323ecce2e93fa2c959a6d52806"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.17" version = "0.2.17"
@@ -373,6 +508,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "percent-encoding"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@@ -384,13 +525,15 @@ name = "podcast-hoarder"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"audiotags",
"chrono", "chrono",
"clap", "clap",
"m3u",
"minreq", "minreq",
"sanitise-file-name", "sanitise-file-name",
"serde", "serde",
"toml", "toml",
"url", "url 2.5.0",
"xml_serde", "xml_serde",
] ]
@@ -406,9 +549,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.76" version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -428,6 +571,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "readme-rustdocifier"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08ad765b21a08b1a8e5cdce052719188a23772bcbefb3c439f0baaf62c56ceac"
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.2" version = "1.10.2"
@@ -552,9 +701,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.48" version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -570,6 +719,26 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.0"
@@ -646,6 +815,17 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
dependencies = [
"idna 0.1.5",
"matches",
"percent-encoding 1.0.1",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.0" version = "2.5.0"
@@ -653,8 +833,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna 0.5.0",
"percent-encoding", "percent-encoding 2.3.1",
] ]
[[package]] [[package]]

View File

@@ -7,8 +7,10 @@ license = "GPL-2.0-only"
[dependencies] [dependencies]
anyhow = "1.0.76" anyhow = "1.0.76"
audiotags = "0.5.0"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
clap = { version = "4.4.11", features = ["derive"] } clap = { version = "4.4.11", features = ["derive"] }
m3u = "1.0.0"
minreq = { version = "2.11.0", features = ["https"] } minreq = { version = "2.11.0", features = ["https"] }
sanitise-file-name = "1.0.0" sanitise-file-name = "1.0.0"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }

View File

@@ -9,9 +9,12 @@ use sanitise_file_name::sanitise;
use crate::rss; use crate::rss;
#[derive(Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct Specification<'a> { pub(crate) struct Specification<'a> {
files: HashMap<Cow<'a, str>, Cow<'a, path::Path>>, 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>>>, feed: BTreeMap<chrono::NaiveDateTime, Vec<Episode<'a>>>,
image_url: Option<Cow<'a, str>>, image_url: Option<Cow<'a, str>>,
} }
@@ -44,9 +47,13 @@ impl<'a> Specification<'a> {
pub(crate) fn feed_iter_mut(&mut self) -> impl Iterator<Item = (&chrono::NaiveDateTime, &mut Vec<Episode<'a>>)> { pub(crate) fn feed_iter_mut(&mut self) -> impl Iterator<Item = (&chrono::NaiveDateTime, &mut Vec<Episode<'a>>)> {
self.feed.iter_mut() self.feed.iter_mut()
} }
pub(crate) fn path_from_id(&self, id: &str) -> Option<&path::Path> {
self.files.get(id).map(|v| &**v)
}
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
pub (crate) struct Episode<'a> { pub (crate) struct Episode<'a> {
/// Episode title. /// Episode title.
title: Cow<'a, str>, title: Cow<'a, str>,
@@ -65,6 +72,10 @@ impl<'a> Episode<'a> {
pub (crate) fn title(&self) -> &str { pub (crate) fn title(&self) -> &str {
self.title.as_ref() self.title.as_ref()
} }
pub(crate) fn id(&self) -> &str {
&self.id
}
} }
fn download_to_file(url: &str, path: &path::Path) -> anyhow::Result<()> { fn download_to_file(url: &str, path: &path::Path) -> anyhow::Result<()> {
@@ -156,7 +167,7 @@ fn update_artwork<'a, 'b>(
match extract_extension_from_url(new.as_ref()) { match extract_extension_from_url(new.as_ref()) {
Ok(Some(extension)) => { Ok(Some(extension)) => {
let cover_path = output.join(format!("cover.{}", extension)); let cover_path = output.join(format!("cover-original.{}", extension));
// Remove cover with conflicting file path if it exists // Remove cover with conflicting file path if it exists
if cover_path.exists() { if cover_path.exists() {

View File

@@ -42,6 +42,12 @@ 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.
Tag {
/// The podcast to tag and generate playlists for.
#[arg(long, short)]
podcast : Option<String>,
},
} }
/// Struct modelling configuration file format. /// Struct modelling configuration file format.

View File

@@ -1,5 +1,6 @@
mod rss; mod rss;
mod input; mod input;
mod tagging;
mod download; mod download;
use input::{Command, ListenStatus}; use input::{Command, ListenStatus};
@@ -28,7 +29,6 @@ fn main() -> anyhow::Result<()> {
anyhow::bail!("could not get parent of configuration path for root directory") anyhow::bail!("could not get parent of configuration path for root directory")
}; };
match args.command { match args.command {
Command::Download { podcast } => { Command::Download { podcast } => {
// Updating single podcast // Updating single podcast
@@ -77,10 +77,18 @@ fn main() -> anyhow::Result<()> {
spec.write_to(&spec_file)?; spec.write_to(&spec_file)?;
}, },
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(()) Ok(())
} }

81
src/tagging.rs Normal file
View File

@@ -0,0 +1,81 @@
use std::{fs, path};
use crate::download;
use anyhow::Context;
use sanitise_file_name::sanitise;
pub(crate) fn generate_m3u(
alias: &str,
root: &path::Path,
) -> anyhow::Result<()> {
let output = root.join(sanitise(&alias));
let spec_file = output.join("spec.toml");
let spec = download::Specification::read_from(&spec_file)?;
let mut playlist = Vec::new();
for episode in spec.feed_iter().map(|(_, eps)| eps.iter()).flatten() {
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)
}));
}
// 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)?;
}
}
Ok(())
}
pub(crate) fn strip_tags(
alias: &str,
root: &path::Path,
) -> anyhow::Result<()> {
let output = root.join(sanitise(&alias));
let spec_file = output.join("spec.toml");
let spec = download::Specification::read_from(&spec_file)?;
for episode in spec.feed_iter().map(|(_, eps)| eps.iter()).flatten() {
let path = output.join(
spec.path_from_id(episode.id()).unwrap()
);
let mut file = audiotags::Tag::new().read_from_path(
&path
)?;
file.remove_title();
file.remove_artist();
file.remove_year();
file.remove_album();
file.set_genre("Podcast");
file.set_title(episode.title());
file.write_to_path(path.as_path().to_str().unwrap())?;
}
Ok(())
}