From ff4543aef9294d77eeef12bf7663a6f1005cb571 Mon Sep 17 00:00:00 2001 From: Tor Date: Mon, 8 Apr 2024 13:54:14 +0200 Subject: [PATCH] new pattern evaluation framework --- Cargo.lock | 37 +++++- Cargo.toml | 2 + src/config.rs | 27 +++-- src/episode.rs | 40 +++++- src/main.rs | 1 + src/patterns.rs | 314 ++++++++++++++++++++++++++++++++++++++++++++++++ src/podcast.rs | 206 ++++++++----------------------- src/tags.rs | 22 ++-- src/utils.rs | 20 ++- 9 files changed, 487 insertions(+), 182 deletions(-) create mode 100644 src/patterns.rs diff --git a/Cargo.lock b/Cargo.lock index fa9b8f9..b2dd772 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,7 +213,7 @@ version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.55", @@ -289,6 +289,8 @@ dependencies = [ "rss", "serde", "serde_json", + "strum", + "strum_macros", "tokio", "toml", "unicode-width", @@ -623,6 +625,15 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.5.0" @@ -1459,6 +1470,24 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "syn" version = "1.0.109" @@ -1702,6 +1731,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode-width" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index 01ceb59..d1b8101 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,4 +28,6 @@ regex = "1.10.4" mime_guess = "2.0.4" quick-xml = "0.31.0" quickxml_to_serde = "0.6.0" +strum = "0.21" +strum_macros = "0.21" diff --git a/src/config.rs b/src/config.rs index 2f5a062..7db99ae 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,5 @@ +use crate::patterns::FullPattern; +use crate::patterns::SourceType; use crate::utils::Unix; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -35,21 +37,21 @@ fn default_name_pattern() -> String { "{pubdate::%Y-%m-%d} {rss::episode::title}".to_string() } -fn default_id_pattern() -> String { - "{guid}".to_string() -} - fn default_download_path() -> String { "{home}/{appname}/{podname}".to_string() } +fn default_id_pattern() -> String { + "{guid}".to_string() +} + /// Configuration for a specific podcast. #[derive(Debug, Clone)] pub struct Config { pub url: String, - pub name_pattern: String, - pub id_pattern: String, - pub download_path: String, + pub name_pattern: FullPattern, + pub id_pattern: FullPattern, + pub download_path: FullPattern, pub id3_tags: HashMap, pub download_hook: Option, pub mode: DownloadMode, @@ -133,16 +135,25 @@ impl Config { .path .unwrap_or_else(|| global_config.path.clone()); + let download_path = FullPattern::from_str(&download_path, vec![SourceType::Podcast]); + let name_pattern = podcast_config .name_pattern .into_val(Some(&global_config.name_pattern)) .unwrap(); + let name_pattern = FullPattern::from_str(&name_pattern, SourceType::all()); + let id_pattern = podcast_config .id_pattern .into_val(global_config.id_pattern.as_ref()) .unwrap_or_else(|| default_id_pattern()); + let id_pattern = FullPattern::from_str( + &id_pattern, + vec![SourceType::Id3, SourceType::Podcast, SourceType::Episode], + ); + let url = podcast_config.url; Self { @@ -180,8 +191,8 @@ impl GlobalConfig { let mut f = std::fs::File::create(&p).unwrap(); f.write_all(s.as_bytes()).unwrap(); } - let str = std::fs::read_to_string(p).unwrap(); + let str = std::fs::read_to_string(p).unwrap(); toml::from_str(&str).unwrap() } } diff --git a/src/episode.rs b/src/episode.rs index 8d301d0..379853d 100644 --- a/src/episode.rs +++ b/src/episode.rs @@ -1,4 +1,4 @@ -use std::fs::File; +use std::path::Path; use std::path::PathBuf; #[derive(Debug, Clone)] @@ -38,7 +38,39 @@ impl<'a> Episode<'a> { } pub struct DownloadedEpisode<'a> { - pub inner: Episode<'a>, - pub file: File, - pub path: PathBuf, + inner: Episode<'a>, + path: PathBuf, +} + +impl<'a> DownloadedEpisode<'a> { + pub fn new(inner: Episode<'a>, path: PathBuf) -> DownloadedEpisode<'a> { + Self { inner, path } + } + + pub fn inner(&self) -> &Episode<'a> { + &self.inner + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn rename(&mut self, new_name: String) { + let new_path = match self.path.extension() { + Some(extension) => { + let mut new_path = self.path.with_file_name(new_name); + new_path.set_extension(extension); + new_path + } + None => self.path.with_file_name(new_name), + }; + + std::fs::rename(&self.path, &new_path).unwrap(); + } +} + +impl<'a> AsRef> for DownloadedEpisode<'a> { + fn as_ref(&self) -> &Episode<'a> { + &self.inner + } } diff --git a/src/main.rs b/src/main.rs index 844bf86..a8fb300 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; mod config; mod episode; mod opml; +mod patterns; mod podcast; mod tags; mod utils; diff --git a/src/patterns.rs b/src/patterns.rs new file mode 100644 index 0000000..9913aa4 --- /dev/null +++ b/src/patterns.rs @@ -0,0 +1,314 @@ +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +use crate::episode::Episode; +use crate::podcast::Podcast; + +use regex::Regex; + +#[derive(PartialEq)] +pub enum SourceType { + Episode, + Podcast, + Id3, +} + +impl SourceType { + pub fn all() -> Vec { + vec![Self::Episode, Self::Podcast, Self::Id3] + } +} + +#[derive(Default, Clone, Copy)] +pub struct DataSources<'a> { + id3: Option<&'a id3::Tag>, + episode: Option<&'a Episode<'a>>, + podcast: Option<&'a Podcast>, +} + +impl<'a> DataSources<'a> { + fn id3(&self) -> &'a id3::Tag { + self.id3.unwrap() + } + + fn episode(&self) -> &'a Episode<'a> { + self.episode.unwrap() + } + + fn podcast(&self) -> &'a Podcast { + self.podcast.unwrap() + } + + pub fn set_episode(mut self, episode: &'a Episode<'a>) -> Self { + self.episode = Some(episode); + self + } + + pub fn set_podcast(mut self, podcast: &'a Podcast) -> Self { + self.podcast = Some(podcast); + self + } + + pub fn set_id3(mut self, id3: &'a id3::Tag) -> Self { + self.id3 = Some(id3); + self + } +} + +#[derive(Debug, Clone)] +pub struct FullPattern(Vec); + +impl FullPattern { + pub fn from_str(s: &str, available_sources: Vec) -> Self { + let mut segments: Vec = vec![]; + let mut text = String::new(); + let mut pattern = String::new(); + + let mut is_inside = false; + + for c in s.chars() { + if c == '}' { + assert!(is_inside); + let pattern = std::mem::take(&mut pattern); + let pattern = Pattern::from_str(&pattern).unwrap(); + if let Some(required_source) = pattern.required_source() { + if !available_sources.contains(&required_source) { + panic!("invalid tag"); + } + } + let segment = Segment::Pattern(pattern); + segments.push(segment); + is_inside = false; + } else if c == '{' { + assert!(!is_inside); + let text = std::mem::take(&mut text); + let segment = Segment::Text(text); + segments.push(segment); + is_inside = true; + } else { + if is_inside { + pattern.push(c); + } else { + text.push(c); + } + } + } + + assert!(!is_inside); + if !text.is_empty() { + segments.push(Segment::Text(text)); + } + + Self(segments) + } +} + +#[derive(Clone, Debug)] +enum Segment { + Text(String), + Pattern(Pattern), +} + +#[derive(Debug, Clone)] +enum Pattern { + Unit(UnitPattern), + Data(DataPattern), +} + +impl Pattern { + fn from_str(s: &str) -> Option { + if let Some(unit) = UnitPattern::from_str(s) { + Some(Self::Unit(unit)) + } else if let Some(data) = DataPattern::from_str(s) { + Some(Self::Data(data)) + } else { + None + } + } + + fn required_source(&self) -> Option { + use DataPattern as DP; + use UnitPattern as UP; + + match self { + Self::Unit(UP::Home) => None, + Self::Unit(UP::AppName) => None, + Self::Unit(UP::PodName) => Some(SourceType::Podcast), + Self::Unit(UP::Url) => Some(SourceType::Episode), + Self::Unit(UP::Guid) => Some(SourceType::Episode), + Self::Data(DP { + ty: DataPatternType::RssChannel, + .. + }) => Some(SourceType::Podcast), + Self::Data(DP { + ty: DataPatternType::RssEpisode, + .. + }) => Some(SourceType::Episode), + Self::Data(DP { + ty: DataPatternType::Id3Tag, + .. + }) => Some(SourceType::Id3), + Self::Data(DP { + ty: DataPatternType::PubDate, + .. + }) => Some(SourceType::Episode), + } + } +} + +#[derive(Clone, Debug)] +struct DataPattern { + ty: DataPatternType, + data: String, +} + +impl DataPattern { + fn from_str(s: &str) -> Option { + for ty in DataPatternType::iter() { + if let Some(caps) = ty.regex().captures(s) { + if let Some(match_str) = caps.get(1) { + return Some(Self { + ty, + data: match_str.as_str().to_owned(), + }); + } + } + } + None + } +} + +impl Evaluate for DataPattern { + fn evaluate(&self, sources: DataSources<'_>) -> String { + use chrono::TimeZone; + use DataPatternType as Ty; + let null = ""; + + match self.ty { + Ty::PubDate => { + let episode = sources.episode(); + let formatting = &self.data; + + let datetime = chrono::Utc.timestamp_opt(episode.published, 0).unwrap(); + + if formatting == "unix" { + episode.published.to_string() + } else { + datetime.format(formatting).to_string() + } + } + Ty::RssEpisode => { + let episode = sources.episode(); + let key = &self.data; + + let key = key.replace(":", crate::utils::NAMESPACE_ALTER); + episode.get_text_value(&key).unwrap_or(null).to_string() + } + Ty::RssChannel => { + let channel = sources.podcast(); + let key = &self.data; + + let key = key.replace(":", crate::utils::NAMESPACE_ALTER); + channel.get_text_attribute(&key).unwrap_or(null).to_string() + } + Ty::Id3Tag => { + use id3::TagLike; + + let tag_key = &self.data; + sources + .id3() + .get(tag_key) + .and_then(|tag| tag.content().text()) + .unwrap_or(null) + .to_string() + } + } + } +} + +#[derive(Clone, Debug, EnumIter)] +enum DataPatternType { + RssEpisode, + RssChannel, + PubDate, + Id3Tag, +} + +impl DataPatternType { + fn regex(&self) -> Regex { + let s = match self { + Self::Id3Tag => "id3", + Self::PubDate => "pubdate", + Self::RssEpisode => "rss::episode", + Self::RssChannel => "rss::channel", + }; + + let s = format!("{}::(.+)", s); + + Regex::new(&s).unwrap() + } +} + +#[derive(Clone, Debug)] +enum UnitPattern { + Guid, + Url, + PodName, + AppName, + Home, +} + +impl UnitPattern { + fn from_str(s: &str) -> Option { + match s { + "guid" => Some(Self::Guid), + "url" => Some(Self::Url), + "podname" => Some(Self::PodName), + "appname" => Some(Self::AppName), + "home" => Some(Self::Home), + _ => None, + } + } +} + +impl Evaluate for UnitPattern { + fn evaluate(&self, sources: DataSources<'_>) -> String { + match self { + Self::Guid => sources.episode().guid.to_string(), + Self::Url => sources.episode().url.to_string(), + Self::PodName => sources.podcast().name().to_string(), + Self::AppName => crate::APPNAME.to_string(), + Self::Home => home(), + } + } +} + +fn home() -> String { + dirs::home_dir() + .unwrap() + .as_os_str() + .to_str() + .unwrap() + .to_owned() +} + +pub trait Evaluate { + fn evaluate(&self, sources: DataSources<'_>) -> String; +} + +impl Evaluate for FullPattern { + fn evaluate(&self, sources: DataSources<'_>) -> String { + let mut output = String::new(); + + for segment in &self.0 { + let text = match segment { + Segment::Text(text) => text.clone(), + Segment::Pattern(Pattern::Unit(pattern)) => pattern.evaluate(sources), + Segment::Pattern(Pattern::Data(pattern)) => pattern.evaluate(sources), + }; + output.push_str(&text); + } + + output + } +} diff --git a/src/podcast.rs b/src/podcast.rs index 5f03613..a2266bf 100644 --- a/src/podcast.rs +++ b/src/podcast.rs @@ -1,8 +1,9 @@ -use crate::config::{Config, GlobalConfig, PodcastConfig}; - use crate::config::DownloadMode; +use crate::config::{Config, GlobalConfig, PodcastConfig}; use crate::episode::DownloadedEpisode; use crate::episode::Episode; +use crate::patterns::DataSources; +use crate::patterns::Evaluate; use crate::utils::current_unix; use crate::utils::get_guid; use crate::utils::remove_xml_namespaces; @@ -10,7 +11,6 @@ use crate::utils::truncate_string; use crate::utils::Unix; use crate::utils::NAMESPACE_ALTER; use futures_util::StreamExt; -use id3::TagLike; use indicatif::MultiProgress; use indicatif::ProgressBar; use indicatif::ProgressStyle; @@ -60,8 +60,8 @@ impl Podcast { } fn download_folder(&self) -> PathBuf { - let download_pattern = &self.config.download_path; - let evaluated = self.evaluate_pattern(download_pattern, None, None); + let data_sources = DataSources::default().set_podcast(self); + let evaluated = self.config.download_path.evaluate(data_sources); let path = PathBuf::from(evaluated); std::fs::create_dir_all(&path).unwrap(); path @@ -92,9 +92,10 @@ impl Podcast { } let config = Config::new(&global_config, config); - let xml_string = Self::load_xml(&config.url).await; + let xml_string = crate::utils::download_text(&config.url).await; let channel = rss::Channel::read_from(xml_string.as_bytes()).unwrap(); let xml_value = xml_to_value(&xml_string); + let progress_bar = match mp { Some(mp) => Some(init_podcast_status(mp, &name)), None => None, @@ -114,7 +115,7 @@ impl Podcast { podcasts } - fn get_text_attribute(&self, key: &str) -> Option<&str> { + pub fn get_text_attribute(&self, key: &str) -> Option<&str> { let rss = self.xml.get("rss").unwrap(); let channel = rss.get("channel").unwrap(); channel.get(key).unwrap().as_str() @@ -161,121 +162,6 @@ impl Podcast { vec } - fn rename_file(&self, file: &Path, tags: Option<&id3::Tag>, episode: &Episode) -> PathBuf { - let pattern = &self.config.name_pattern; - let result = self.evaluate_pattern(pattern, tags, Some(episode)); - - let new_name = match file.extension() { - Some(extension) => { - let mut new_path = file.with_file_name(result); - new_path.set_extension(extension); - new_path - } - None => file.with_file_name(result), - }; - - std::fs::rename(file, &new_name).unwrap(); - new_name - } - - fn evaluate_pattern( - &self, - pattern: &str, - tags: Option<&id3::Tag>, - episode: Option<&Episode>, - ) -> String { - let null = ""; - let re = regex::Regex::new(r"\{([^\}]+)\}").unwrap(); - - let mut result = String::new(); - let mut last_end = 0; - - use chrono::TimeZone; - - for cap in re.captures_iter(&pattern) { - let match_range = cap.get(0).unwrap().range(); - let key = &cap[1]; - - result.push_str(&pattern[last_end..match_range.start]); - - let replacement = match key { - date if date.starts_with("pubdate::") && episode.is_some() => { - let episode = episode.unwrap(); - let datetime = chrono::Utc.timestamp_opt(episode.published, 0).unwrap(); - let (_, format) = date.split_once("::").unwrap(); - if format == "unix" { - episode.published.to_string() - } else { - datetime.format(format).to_string() - } - } - id3 if id3.starts_with("id3::") && tags.is_some() => { - let (_, tag) = id3.split_once("::").unwrap(); - tags.unwrap() - .get(tag) - .and_then(|tag| tag.content().text()) - .unwrap_or(null) - .to_string() - } - rss if rss.starts_with("rss::episode::") && episode.is_some() => { - let episode = episode.unwrap(); - let (_, key) = rss.split_once("episode::").unwrap(); - - let key = key.replace(":", NAMESPACE_ALTER); - episode.get_text_value(&key).unwrap_or(null).to_string() - } - rss if rss.starts_with("rss::channel::") => { - let (_, key) = rss.split_once("channel::").unwrap(); - - let key = key.replace(":", NAMESPACE_ALTER); - self.get_text_attribute(&key).unwrap_or(null).to_string() - } - - "guid" if episode.is_some() => episode.unwrap().guid.to_string(), - "url" if episode.is_some() => episode.unwrap().url.to_string(), - "podname" => self.name.clone(), - "appname" => crate::APPNAME.to_string(), - "home" => dirs::home_dir() - .unwrap() - .as_os_str() - .to_str() - .unwrap() - .to_owned(), - invalid_tag => { - eprintln!("invalid tag configured: {}", invalid_tag); - std::process::exit(1); - } - }; - - result.push_str(&replacement); - - last_end = match_range.end; - } - - result.push_str(&pattern[last_end..]); - result - } - - async fn load_xml(url: &str) -> String { - let response = reqwest::Client::new() - .get(url) - .header( - "User-Agent", - "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0", - ) - .send() - .await - .unwrap(); - - if response.status().is_success() { - let xml = response.text().await.unwrap(); - - xml - } else { - panic!("failed to get response or smth"); - } - } - fn should_download( &self, episode: &Episode, @@ -327,15 +213,19 @@ impl Podcast { } } - fn mark_downloaded(&self, episode: &Episode) { - let path = self.download_folder(); - let id = self.get_id(episode); - DownloadedEpisodes::append(&id, &path, &episode); + fn mark_downloaded(&self, episode: &DownloadedEpisode) { + let id = self.get_id(episode.inner()); + DownloadedEpisodes::append(&id, &episode); } fn get_id(&self, episode: &Episode) -> String { - let id_pattern = &self.config.id_pattern; - self.evaluate_pattern(id_pattern, None, Some(episode)) + let data_sources = DataSources::default() + .set_podcast(self) + .set_episode(episode); + + self.config + .id_pattern + .evaluate(data_sources) .replace(" ", "_") } @@ -365,7 +255,7 @@ impl Podcast { episodes } - fn set_download_style(&self) { + fn show_download_bar(&self) { if let Some(pb) = &self.progress_bar { pb.set_style( ProgressStyle::default_bar() @@ -479,27 +369,24 @@ impl Podcast { std::fs::rename(partial_path, &path).unwrap(); - DownloadedEpisode { - inner: episode, - file, - path, - } + DownloadedEpisode::new(episode, path) } async fn normalize_episode(&self, episode: &mut DownloadedEpisode<'_>) { - let file_path = &episode.path; - let mp3_tags = (file_path.extension().unwrap() == "mp3").then_some( - crate::tags::set_mp3_tags( - &self.channel, - &episode.inner, - file_path, - &self.config.id3_tags, + let mp3_tags = (episode.path().extension().unwrap() == "mp3") + .then_some( + crate::tags::set_mp3_tags(&self.channel, episode, &self.config.id3_tags).await, ) - .await, - ); + .unwrap_or_default(); + + let datasource = DataSources::default() + .set_id3(&mp3_tags) + .set_episode(episode.inner()) + .set_podcast(self); - let new_path = self.rename_file(&file_path, mp3_tags.as_ref(), &episode.inner); - episode.path = new_path; + let file_name = self.config().name_pattern.evaluate(datasource); + + episode.rename(file_name); } fn run_download_hook( @@ -507,10 +394,13 @@ impl Podcast { episode: &DownloadedEpisode, ) -> Option> { let script_path = self.config.download_hook.clone()?; - let path = episode.path.clone(); + let path = episode.path().to_owned(); let handle = tokio::task::spawn_blocking(move || { - let _ = std::process::Command::new(script_path).arg(path).output(); + std::process::Command::new(script_path) + .arg(path) + .output() + .unwrap(); }); Some(handle) @@ -525,7 +415,7 @@ impl Podcast { } pub async fn sync(&self, longest_podcast_name: usize) -> Vec { - self.set_download_style(); + self.show_download_bar(); let episodes = self.pending_episodes(); let episode_qty = episodes.len(); @@ -537,8 +427,8 @@ impl Podcast { self.show_download_info(&episode, index, longest_podcast_name, episode_qty); let mut downloaded_episode = self.download_episode(episode).await; self.normalize_episode(&mut downloaded_episode).await; - self.mark_downloaded(&downloaded_episode.inner); hook_handles.extend(self.run_download_hook(&downloaded_episode)); + self.mark_downloaded(&downloaded_episode); downloaded.push(downloaded_episode); } @@ -548,7 +438,10 @@ impl Podcast { } self.mark_complete(); - downloaded.into_iter().map(|episode| episode.path).collect() + downloaded + .into_iter() + .map(|episode| episode.path().to_owned()) + .collect() } } @@ -587,8 +480,8 @@ impl DownloadedEpisodes { Self(hashmap) } - fn append(id: &str, path: &Path, episode: &Episode) { - let path = path.join(Self::FILENAME); + fn append(id: &str, episode: &DownloadedEpisode) { + let path = episode.path().parent().unwrap().join(Self::FILENAME); use std::io::Write; let mut file = std::fs::OpenOptions::new() @@ -597,6 +490,13 @@ impl DownloadedEpisodes { .open(path) .unwrap(); - writeln!(file, "{} {} \"{}\"", id, current_unix(), &episode.title).unwrap(); + writeln!( + file, + "{} {} \"{}\"", + id, + current_unix(), + episode.as_ref().title + ) + .unwrap(); } } diff --git a/src/tags.rs b/src/tags.rs index aa7a6ed..8421df7 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -1,4 +1,4 @@ -use crate::episode::Episode; +use crate::episode::DownloadedEpisode; use chrono::Datelike; use id3::TagLike; use std::collections::HashMap; @@ -42,22 +42,19 @@ async fn add_picture(tag: &mut id3::Tag, picture_type: id3::frame::PictureType, } fn has_picture_type(tag: &id3::Tag, ty: id3::frame::PictureType) -> bool { - for pic in tag.pictures() { - if pic.picture_type == ty { - return true; - } - } - - false + tag.pictures().any(|pic| pic.picture_type == ty) } pub async fn set_mp3_tags<'a>( channel: &'a rss::Channel, - episode: &'a Episode<'a>, - file_path: &std::path::Path, + episode: &'a DownloadedEpisode<'a>, custom_tags: &HashMap, ) -> id3::Tag { - let mut tags = id3::Tag::read_from_path(&file_path).unwrap(); + let file_path = &episode.path(); + let episode = &episode.inner(); + + let mut tags = id3::Tag::read_from_path(file_path).unwrap(); + for (id, value) in custom_tags { tags.set_text(id, value); } @@ -174,8 +171,7 @@ pub async fn set_mp3_tags<'a>( tags.set_text(Id3Tag::PODCAST_ID, episode.guid); } - tags.write_to_path(&file_path, id3::Version::Id3v24) - .unwrap(); + tags.write_to_path(file_path, id3::Version::Id3v24).unwrap(); tags } diff --git a/src/utils.rs b/src/utils.rs index 27d613c..d0baaf9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,8 +2,10 @@ use quick_xml::{ events::{BytesEnd, BytesStart, Event}, Reader, Writer, }; +use serde::Serialize; use serde_json::Value; use std::borrow::Cow; +use std::collections::HashMap; use std::io::Cursor; use std::io::Write as IOWrite; use std::path::PathBuf; @@ -222,9 +224,6 @@ pub fn truncate_string(s: &str, max_width: usize) -> String { truncated } -use serde::Serialize; -use std::collections::HashMap; - #[derive(Serialize)] struct BasicPodcast { url: String, @@ -265,6 +264,21 @@ pub fn append_podcasts(name_and_url: Vec<(String, String)>) { std::fs::write(&path, new_config).unwrap(); } +pub async fn download_text(url: &str) -> String { + reqwest::Client::new() + .get(url) + .header( + "User-Agent", + "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0", + ) + .send() + .await + .unwrap() + .text() + .await + .unwrap() +} + #[cfg(test)] mod tests { use super::*;