use super::common::{MimeType, Url};
#[derive(Debug, Clone, Default)]
pub struct ItunesFeedMeta {
pub author: Option<String>,
pub owner: Option<ItunesOwner>,
pub categories: Vec<ItunesCategory>,
pub explicit: Option<bool>,
pub image: Option<Url>,
pub keywords: Vec<String>,
pub podcast_type: Option<String>,
pub complete: Option<String>,
pub new_feed_url: Option<Url>,
pub subtitle: Option<String>,
pub summary: Option<String>,
pub block: Option<u8>,
}
#[derive(Debug, Clone, Default)]
pub struct ItunesEntryMeta {
pub title: Option<String>,
pub author: Option<String>,
pub duration: Option<String>,
pub explicit: Option<bool>,
pub image: Option<Url>,
pub episode: Option<String>,
pub season: Option<String>,
pub episode_type: Option<String>,
pub subtitle: Option<String>,
pub summary: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ItunesOwner {
pub name: Option<String>,
pub email: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ItunesCategory {
pub text: String,
pub subcategory: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct PodcastMeta {
pub transcripts: Vec<PodcastTranscript>,
pub funding: Vec<PodcastFunding>,
pub persons: Vec<PodcastPerson>,
pub guid: Option<String>,
pub value: Option<PodcastValue>,
pub medium: Option<String>,
pub locked: Option<String>,
pub locked_owner: Option<String>,
pub location: Option<PodcastLocation>,
pub podroll: Vec<PodcastRemoteItem>,
pub txt: Vec<PodcastTxt>,
pub update_frequency: Option<PodcastUpdateFrequency>,
pub follow: Vec<PodcastFollow>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastValue {
pub type_: String,
pub method: String,
pub suggested: Option<String>,
pub recipients: Vec<PodcastValueRecipient>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastValueRecipient {
pub name: Option<String>,
pub type_: String,
pub address: String,
pub split: u32,
pub fee: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PodcastTranscript {
pub url: Url,
pub transcript_type: Option<MimeType>,
pub language: Option<String>,
pub rel: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PodcastFunding {
pub url: Url,
pub message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PodcastPerson {
pub name: String,
pub role: Option<String>,
pub group: Option<String>,
pub img: Option<Url>,
pub href: Option<Url>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastChapters {
pub url: Url,
pub type_: MimeType,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[allow(clippy::derive_partial_eq_without_eq)]
pub struct PodcastSoundbite {
pub start_time: f64,
pub duration: f64,
pub title: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastAlternateEnclosureSource {
pub uri: Url,
pub content_type: Option<MimeType>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastIntegrity {
pub type_: String,
pub value: String,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[allow(clippy::derive_partial_eq_without_eq)]
pub struct PodcastAlternateEnclosure {
pub type_: MimeType,
pub length: Option<u64>,
pub bitrate: Option<f64>,
pub height: Option<u32>,
pub lang: Option<String>,
pub title: Option<String>,
pub rel: Option<String>,
pub codecs: Option<String>,
pub default: Option<bool>,
pub sources: Vec<PodcastAlternateEnclosureSource>,
pub integrity: Option<PodcastIntegrity>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastLocation {
pub name: String,
pub geo: Option<String>,
pub osm: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastRemoteItem {
pub feed_guid: Option<String>,
pub feed_url: Option<Url>,
pub item_guid: Option<String>,
pub medium: Option<String>,
pub title: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastSocialInteract {
pub uri: Url,
pub protocol: Option<String>,
pub account_id: Option<String>,
pub account_url: Option<Url>,
pub priority: Option<u32>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastTxt {
pub purpose: Option<String>,
pub value: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastUpdateFrequency {
pub rrule: Option<String>,
pub complete: Option<bool>,
pub dtstart: Option<String>,
pub label: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodcastFollow {
pub url: Url,
pub platform: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PodcastEntryMeta {
pub transcript: Vec<PodcastTranscript>,
pub chapters: Option<PodcastChapters>,
pub soundbite: Vec<PodcastSoundbite>,
pub persons: Vec<PodcastPerson>,
pub medium: Option<String>,
pub season: Option<String>,
pub episode: Option<String>,
pub alternate_enclosures: Vec<PodcastAlternateEnclosure>,
pub location: Option<PodcastLocation>,
pub social_interact: Vec<PodcastSocialInteract>,
pub txt: Vec<PodcastTxt>,
pub follow: Vec<PodcastFollow>,
}
pub fn parse_explicit(s: &str) -> Option<bool> {
let s = s.trim();
if s.eq_ignore_ascii_case("yes")
|| s.eq_ignore_ascii_case("true")
|| s.eq_ignore_ascii_case("explicit")
{
Some(true)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_explicit_true_variants() {
assert_eq!(parse_explicit("yes"), Some(true));
assert_eq!(parse_explicit("YES"), Some(true));
assert_eq!(parse_explicit("Yes"), Some(true));
assert_eq!(parse_explicit("true"), Some(true));
assert_eq!(parse_explicit("TRUE"), Some(true));
assert_eq!(parse_explicit("explicit"), Some(true));
assert_eq!(parse_explicit("EXPLICIT"), Some(true));
}
#[test]
fn test_parse_explicit_false_variants_return_none() {
assert_eq!(parse_explicit("no"), None);
assert_eq!(parse_explicit("NO"), None);
assert_eq!(parse_explicit("No"), None);
assert_eq!(parse_explicit("false"), None);
assert_eq!(parse_explicit("FALSE"), None);
assert_eq!(parse_explicit("clean"), None);
assert_eq!(parse_explicit("CLEAN"), None);
}
#[test]
fn test_parse_explicit_whitespace() {
assert_eq!(parse_explicit(" yes "), Some(true));
assert_eq!(parse_explicit(" no "), None);
}
#[test]
fn test_parse_explicit_unknown() {
assert_eq!(parse_explicit("unknown"), None);
assert_eq!(parse_explicit("maybe"), None);
assert_eq!(parse_explicit(""), None);
assert_eq!(parse_explicit("1"), None);
}
#[test]
fn test_itunes_feed_meta_default() {
let meta = ItunesFeedMeta::default();
assert!(meta.author.is_none());
assert!(meta.owner.is_none());
assert!(meta.categories.is_empty());
assert!(meta.explicit.is_none());
assert!(meta.image.is_none());
assert!(meta.keywords.is_empty());
assert!(meta.podcast_type.is_none());
assert!(meta.complete.is_none());
assert!(meta.new_feed_url.is_none());
}
#[test]
fn test_itunes_entry_meta_default() {
let meta = ItunesEntryMeta::default();
assert!(meta.title.is_none());
assert!(meta.author.is_none());
assert!(meta.duration.is_none());
assert!(meta.explicit.is_none());
assert!(meta.image.is_none());
assert!(meta.episode.is_none());
assert!(meta.season.is_none());
assert!(meta.episode_type.is_none());
}
#[test]
fn test_itunes_entry_meta_string_fields() {
let meta = ItunesEntryMeta {
duration: Some("1:23:45".to_string()),
episode: Some("42".to_string()),
season: Some("3".to_string()),
..Default::default()
};
assert_eq!(meta.duration.as_deref(), Some("1:23:45"));
assert_eq!(meta.episode.as_deref(), Some("42"));
assert_eq!(meta.season.as_deref(), Some("3"));
}
#[test]
fn test_itunes_owner_default() {
let owner = ItunesOwner::default();
assert!(owner.name.is_none());
assert!(owner.email.is_none());
}
#[test]
#[allow(clippy::redundant_clone)]
fn test_itunes_category_clone() {
let category = ItunesCategory {
text: "Technology".to_string(),
subcategory: Some("Software".to_string()),
};
let cloned = category.clone();
assert_eq!(cloned.text, "Technology");
assert_eq!(cloned.subcategory.as_deref(), Some("Software"));
}
#[test]
fn test_podcast_meta_default() {
let meta = PodcastMeta::default();
assert!(meta.transcripts.is_empty());
assert!(meta.funding.is_empty());
assert!(meta.persons.is_empty());
assert!(meta.guid.is_none());
assert!(meta.location.is_none());
assert!(meta.podroll.is_empty());
assert!(meta.txt.is_empty());
assert!(meta.update_frequency.is_none());
assert!(meta.follow.is_empty());
}
#[test]
fn test_podcast_entry_meta_new_fields_default() {
let meta = PodcastEntryMeta::default();
assert!(meta.alternate_enclosures.is_empty());
assert!(meta.location.is_none());
assert!(meta.social_interact.is_empty());
assert!(meta.txt.is_empty());
assert!(meta.follow.is_empty());
}
#[test]
fn test_podcast_location_default() {
let loc = PodcastLocation::default();
assert!(loc.name.is_empty());
assert!(loc.geo.is_none());
assert!(loc.osm.is_none());
}
#[test]
fn test_podcast_social_interact_default() {
let si = PodcastSocialInteract::default();
assert!(si.uri.is_empty());
assert!(si.protocol.is_none());
assert!(si.account_id.is_none());
assert!(si.account_url.is_none());
assert!(si.priority.is_none());
}
#[test]
fn test_podcast_txt_default() {
let txt = PodcastTxt::default();
assert!(txt.purpose.is_none());
assert!(txt.value.is_empty());
}
#[test]
fn test_podcast_update_frequency_default() {
let uf = PodcastUpdateFrequency::default();
assert!(uf.rrule.is_none());
assert!(uf.complete.is_none());
assert!(uf.dtstart.is_none());
assert!(uf.label.is_none());
}
#[test]
fn test_podcast_follow_default() {
let f = PodcastFollow::default();
assert!(f.url.is_empty());
assert!(f.platform.is_none());
}
#[test]
fn test_podcast_remote_item_default() {
let item = PodcastRemoteItem::default();
assert!(item.feed_guid.is_none());
assert!(item.feed_url.is_none());
assert!(item.item_guid.is_none());
assert!(item.medium.is_none());
assert!(item.title.is_none());
}
#[test]
fn test_podcast_alternate_enclosure_default() {
let ae = PodcastAlternateEnclosure::default();
assert!(ae.type_.is_empty());
assert!(ae.length.is_none());
assert!(ae.bitrate.is_none());
assert!(ae.sources.is_empty());
assert!(ae.integrity.is_none());
}
#[test]
#[allow(clippy::redundant_clone)]
fn test_podcast_transcript_clone() {
let transcript = PodcastTranscript {
url: "https://example.com/transcript.txt".to_string().into(),
transcript_type: Some("text/plain".to_string().into()),
language: Some("en".to_string()),
rel: None,
};
let cloned = transcript.clone();
assert_eq!(cloned.url, "https://example.com/transcript.txt");
assert_eq!(cloned.transcript_type.as_deref(), Some("text/plain"));
}
#[test]
#[allow(clippy::redundant_clone)]
fn test_podcast_funding_clone() {
let funding = PodcastFunding {
url: "https://example.com/donate".to_string().into(),
message: Some("Support us!".to_string()),
};
let cloned = funding.clone();
assert_eq!(cloned.url, "https://example.com/donate");
assert_eq!(cloned.message.as_deref(), Some("Support us!"));
}
#[test]
#[allow(clippy::redundant_clone)]
fn test_podcast_person_clone() {
let person = PodcastPerson {
name: "John Doe".to_string(),
role: Some("host".to_string()),
group: None,
img: Some("https://example.com/john.jpg".to_string().into()),
href: Some("https://example.com".to_string().into()),
};
let cloned = person.clone();
assert_eq!(cloned.name, "John Doe");
assert_eq!(cloned.role.as_deref(), Some("host"));
}
#[test]
fn test_podcast_chapters_default() {
let chapters = PodcastChapters::default();
assert!(chapters.url.is_empty());
assert!(chapters.type_.is_empty());
}
#[test]
#[allow(clippy::redundant_clone)]
fn test_podcast_chapters_clone() {
let chapters = PodcastChapters {
url: "https://example.com/chapters.json".to_string().into(),
type_: "application/json+chapters".to_string().into(),
};
let cloned = chapters.clone();
assert_eq!(cloned.url, "https://example.com/chapters.json");
assert_eq!(cloned.type_, "application/json+chapters");
}
#[test]
fn test_podcast_soundbite_default() {
let soundbite = PodcastSoundbite::default();
assert!((soundbite.start_time - 0.0).abs() < f64::EPSILON);
assert!((soundbite.duration - 0.0).abs() < f64::EPSILON);
assert!(soundbite.title.is_none());
}
#[test]
#[allow(clippy::redundant_clone)]
fn test_podcast_soundbite_clone() {
let soundbite = PodcastSoundbite {
start_time: 120.5,
duration: 30.0,
title: Some("Great quote".to_string()),
};
let cloned = soundbite.clone();
assert!((cloned.start_time - 120.5).abs() < f64::EPSILON);
assert!((cloned.duration - 30.0).abs() < f64::EPSILON);
assert_eq!(cloned.title.as_deref(), Some("Great quote"));
}
#[test]
fn test_podcast_entry_meta_default() {
let meta = PodcastEntryMeta::default();
assert!(meta.transcript.is_empty());
assert!(meta.chapters.is_none());
assert!(meta.soundbite.is_empty());
assert!(meta.persons.is_empty());
assert!(meta.medium.is_none());
}
#[test]
fn test_itunes_feed_meta_new_fields() {
let meta = ItunesFeedMeta {
complete: Some("Yes".to_string()),
new_feed_url: Some("https://example.com/new-feed.xml".to_string().into()),
..Default::default()
};
assert_eq!(meta.complete.as_deref(), Some("Yes"));
assert_eq!(
meta.new_feed_url.as_deref(),
Some("https://example.com/new-feed.xml")
);
}
#[test]
fn test_podcast_value_default() {
let value = PodcastValue::default();
assert!(value.type_.is_empty());
assert!(value.method.is_empty());
assert!(value.suggested.is_none());
assert!(value.recipients.is_empty());
}
#[test]
fn test_podcast_value_lightning() {
let value = PodcastValue {
type_: "lightning".to_string(),
method: "keysend".to_string(),
suggested: Some("0.00000005000".to_string()),
recipients: vec![
PodcastValueRecipient {
name: Some("Host".to_string()),
type_: "node".to_string(),
address: "03ae9f91a0cb8ff43840e3c322c4c61f019d8c1c3cea15a25cfc425ac605e61a4a"
.to_string(),
split: 90,
fee: Some(false),
},
PodcastValueRecipient {
name: Some("Producer".to_string()),
type_: "node".to_string(),
address: "02d5c1bf8b940dc9cadca86d1b0a3c37fbe39cee4c7e839e33bef9174531d27f52"
.to_string(),
split: 10,
fee: Some(false),
},
],
};
assert_eq!(value.type_, "lightning");
assert_eq!(value.method, "keysend");
assert_eq!(value.suggested.as_deref(), Some("0.00000005000"));
assert_eq!(value.recipients.len(), 2);
assert_eq!(value.recipients[0].split, 90);
assert_eq!(value.recipients[1].split, 10);
}
#[test]
fn test_podcast_value_recipient_default() {
let recipient = PodcastValueRecipient::default();
assert!(recipient.name.is_none());
assert!(recipient.type_.is_empty());
assert!(recipient.address.is_empty());
assert_eq!(recipient.split, 0);
assert!(recipient.fee.is_none());
}
#[test]
fn test_podcast_value_recipient_with_fee() {
let recipient = PodcastValueRecipient {
name: Some("Hosting Provider".to_string()),
type_: "node".to_string(),
address: "02d5c1bf8b940dc9cadca86d1b0a3c37fbe39cee4c7e839e33bef9174531d27f52"
.to_string(),
split: 5,
fee: Some(true),
};
assert_eq!(recipient.name.as_deref(), Some("Hosting Provider"));
assert_eq!(recipient.split, 5);
assert_eq!(recipient.fee, Some(true));
}
#[test]
fn test_podcast_value_recipient_without_name() {
let recipient = PodcastValueRecipient {
name: None,
type_: "node".to_string(),
address: "03ae9f91a0cb8ff43840e3c322c4c61f019d8c1c3cea15a25cfc425ac605e61a4a"
.to_string(),
split: 100,
fee: Some(false),
};
assert!(recipient.name.is_none());
assert_eq!(recipient.split, 100);
}
#[test]
fn test_podcast_value_multiple_recipients() {
let mut value = PodcastValue {
type_: "lightning".to_string(),
method: "keysend".to_string(),
suggested: None,
recipients: Vec::new(),
};
for i in 1..=5 {
value.recipients.push(PodcastValueRecipient {
name: Some(format!("Recipient {i}")),
type_: "node".to_string(),
address: format!("address_{i}"),
split: 20,
fee: Some(false),
});
}
assert_eq!(value.recipients.len(), 5);
assert_eq!(value.recipients.iter().map(|r| r.split).sum::<u32>(), 100);
}
#[test]
fn test_podcast_value_hive() {
let value = PodcastValue {
type_: "hive".to_string(),
method: "direct".to_string(),
suggested: Some("1.00000".to_string()),
recipients: vec![PodcastValueRecipient {
name: Some("@username".to_string()),
type_: "account".to_string(),
address: "username".to_string(),
split: 100,
fee: Some(false),
}],
};
assert_eq!(value.type_, "hive");
assert_eq!(value.method, "direct");
}
#[test]
fn test_podcast_meta_with_value() {
let mut meta = PodcastMeta::default();
assert!(meta.value.is_none());
meta.value = Some(PodcastValue {
type_: "lightning".to_string(),
method: "keysend".to_string(),
suggested: Some("0.00000005000".to_string()),
recipients: vec![],
});
assert!(meta.value.is_some());
assert_eq!(meta.value.as_ref().unwrap().type_, "lightning");
}
#[test]
#[allow(clippy::redundant_clone)]
fn test_podcast_value_clone() {
let value = PodcastValue {
type_: "lightning".to_string(),
method: "keysend".to_string(),
suggested: Some("0.00000005000".to_string()),
recipients: vec![PodcastValueRecipient {
name: Some("Host".to_string()),
type_: "node".to_string(),
address: "abc123".to_string(),
split: 100,
fee: Some(false),
}],
};
let cloned = value.clone();
assert_eq!(cloned.type_, "lightning");
assert_eq!(cloned.recipients.len(), 1);
assert_eq!(cloned.recipients[0].name.as_deref(), Some("Host"));
}
}