use std::fmt::Debug;
use std::time::Duration;
use chrono::{DateTime, Utc};
use mediatype::{MediaTypeBuf, names};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use url::Url;
#[cfg(test)]
use crate::parser::util::parse_timestamp_lenient;
use crate::parser::{ParseErrorKind, ParseFeedError, util};
#[derive(Clone, Deserialize, PartialEq, Serialize)]
pub struct Feed {
pub feed_type: FeedType,
pub id: String,
pub title: Option<Text>,
pub updated: Option<DateTime<Utc>>,
pub authors: Vec<Person>,
pub description: Option<Text>,
pub links: Vec<Link>,
pub categories: Vec<Category>,
pub contributors: Vec<Person>,
pub generator: Option<Generator>,
pub icon: Option<Image>,
pub language: Option<String>,
pub logo: Option<Image>,
pub published: Option<DateTime<Utc>>,
pub rating: Option<MediaRating>,
pub rights: Option<Text>,
pub ttl: Option<u32>,
pub people: Vec<PodcastPerson>,
pub entries: Vec<Entry>,
}
impl Feed {
pub(crate) fn new(feed_type: FeedType) -> Self {
Feed {
feed_type,
id: "".into(),
title: None,
updated: None,
authors: Vec::new(),
description: None,
links: Vec::new(),
categories: Vec::new(),
contributors: Vec::new(),
generator: None,
icon: None,
language: None,
logo: None,
published: None,
rating: None,
rights: None,
ttl: None,
people: Vec::new(),
entries: Vec::new(),
}
}
}
impl Debug for Feed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
f.write_str(serde_json::to_string(self).unwrap().as_str())
}
}
#[cfg(test)]
impl Feed {
pub fn author(mut self, person: Person) -> Self {
self.authors.push(person);
self
}
pub fn category(mut self, category: Category) -> Self {
self.categories.push(category);
self
}
pub fn contributor(mut self, person: Person) -> Self {
self.contributors.push(person);
self
}
pub fn description(mut self, description: Text) -> Self {
self.description = Some(description);
self
}
pub fn entry(mut self, entry: Entry) -> Self {
self.entries.push(entry);
self
}
pub fn generator(mut self, generator: Generator) -> Self {
self.generator = Some(generator);
self
}
pub fn icon(mut self, image: Image) -> Self {
self.icon = Some(image);
self
}
pub fn id(mut self, id: &str) -> Self {
self.id = id.to_string();
self
}
pub fn language(mut self, language: &str) -> Self {
self.language = Some(language.to_owned());
self
}
pub fn link(mut self, link: Link) -> Self {
self.links.push(link);
self
}
pub fn logo(mut self, image: Image) -> Self {
self.logo = Some(image);
self
}
pub fn published(mut self, pub_date: &str) -> Self {
self.published = parse_timestamp_lenient(pub_date);
self
}
pub fn rights(mut self, rights: Text) -> Self {
self.rights = Some(rights);
self
}
pub fn title(mut self, title: Text) -> Self {
self.title = Some(title);
self
}
pub fn ttl(mut self, ttl: u32) -> Self {
self.ttl = Some(ttl);
self
}
pub fn updated(mut self, updated: Option<DateTime<Utc>>) -> Self {
self.updated = updated;
self
}
pub fn updated_parsed(mut self, updated: &str) -> Self {
self.updated = parse_timestamp_lenient(updated);
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum FeedType {
Atom,
JSON,
RSS0,
RSS1,
RSS2,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Entry {
pub id: String,
pub title: Option<Text>,
pub updated: Option<DateTime<Utc>>,
pub authors: Vec<Person>,
pub content: Option<Content>,
pub links: Vec<Link>,
pub summary: Option<Text>,
pub categories: Vec<Category>,
pub contributors: Vec<Person>,
pub published: Option<DateTime<Utc>>,
pub source: Option<String>,
pub rights: Option<Text>,
pub media: Vec<MediaObject>,
pub language: Option<String>,
pub base: Option<String>,
}
impl Default for Entry {
fn default() -> Self {
Entry {
id: "".into(),
title: None,
updated: None,
authors: Vec::new(),
content: None,
links: Vec::new(),
summary: None,
categories: Vec::new(),
contributors: Vec::new(),
published: None,
source: None,
rights: None,
media: Vec::new(),
language: None,
base: None,
}
}
}
#[cfg(test)]
impl Entry {
pub fn author(mut self, person: Person) -> Self {
self.authors.push(person);
self
}
pub fn category(mut self, category: Category) -> Self {
self.categories.push(category);
self
}
pub fn content(mut self, content: Content) -> Self {
self.content = Some(content);
self
}
pub fn contributor(mut self, person: Person) -> Self {
self.contributors.push(person);
self
}
pub fn id(mut self, id: &str) -> Self {
self.id = id.trim().to_string();
self
}
pub fn link(mut self, link: Link) -> Self {
self.links.push(link);
self
}
pub fn published(mut self, published: &str) -> Self {
self.published = parse_timestamp_lenient(published);
self
}
pub fn rights(mut self, rights: Text) -> Self {
self.rights = Some(rights);
self
}
pub fn summary(mut self, summary: Text) -> Self {
self.summary = Some(summary);
self
}
pub fn title(mut self, title: Text) -> Self {
self.title = Some(title);
self
}
pub fn updated(mut self, updated: Option<DateTime<Utc>>) -> Self {
self.updated = updated;
self
}
pub fn updated_parsed(mut self, updated: &str) -> Self {
self.updated = parse_timestamp_lenient(updated);
self
}
pub fn media(mut self, media: MediaObject) -> Self {
self.media.push(media);
self
}
pub fn language(mut self, language: &str) -> Self {
self.language = Some(language.to_owned());
self
}
pub fn base(mut self, url: &str) -> Self {
self.base = Some(url.to_owned());
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Category {
pub term: String,
pub scheme: Option<String>,
pub label: Option<String>,
pub subcategories: Vec<Category>,
}
impl Category {
pub fn new(term: &str) -> Category {
Category {
term: term.trim().into(),
scheme: None,
label: None,
subcategories: Vec::new(),
}
}
}
#[cfg(test)]
impl Category {
pub fn label(mut self, label: &str) -> Self {
self.label = Some(label.to_owned());
self
}
pub fn scheme(mut self, scheme: &str) -> Self {
self.scheme = Some(scheme.to_owned());
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Content {
pub body: Option<String>,
pub content_type: MediaTypeBuf,
pub length: Option<u64>,
pub src: Option<Link>,
}
impl Default for Content {
fn default() -> Content {
Content {
body: None,
content_type: MediaTypeBuf::new(names::TEXT, names::PLAIN),
length: None,
src: None,
}
}
}
impl Content {
pub fn sanitize(&mut self) {
#[cfg(feature = "sanitize")]
{
let content_type = self.content_type.as_str();
if content_type == "text/html" || content_type == "application/xhtml+xml" {
if let Some(body) = &self.body {
self.body = Some(ammonia::clean(body.as_str()));
}
}
}
}
}
#[cfg(test)]
impl Content {
pub fn body(mut self, body: &str) -> Self {
self.body = Some(body.to_owned());
self
}
pub fn content_type(mut self, content_type: &str) -> Self {
self.content_type = content_type.parse::<MediaTypeBuf>().unwrap();
self
}
pub fn length(mut self, length: u64) -> Self {
self.length = Some(length);
self
}
pub fn src(mut self, url: &str) -> Self {
self.src = Some(Link::new(url, None));
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Generator {
pub content: String,
pub uri: Option<String>,
pub version: Option<String>,
}
impl Generator {
pub(crate) fn new(content: &str) -> Generator {
Generator {
uri: None,
version: None,
content: content.trim().into(),
}
}
}
#[cfg(test)]
impl Generator {
pub fn uri(mut self, uri: &str) -> Self {
self.uri = Some(uri.to_owned());
self
}
pub fn version(mut self, version: &str) -> Self {
self.version = Some(version.to_owned());
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Image {
pub uri: String,
pub title: Option<String>,
pub link: Option<Link>,
pub width: Option<u32>,
pub height: Option<u32>,
pub description: Option<String>,
}
impl Image {
pub(crate) fn new(uri: String) -> Image {
Image {
uri,
title: None,
link: None,
width: None,
height: None,
description: None,
}
}
}
#[cfg(test)]
impl Image {
pub fn description(mut self, description: &str) -> Self {
self.description = Some(description.to_owned());
self
}
pub fn height(mut self, height: u32) -> Self {
self.height = Some(height);
self
}
pub fn link(mut self, link: &str) -> Self {
self.link = Some(Link::new(link, None));
self
}
pub fn title(mut self, title: &str) -> Self {
self.title = Some(title.to_owned());
self
}
pub fn width(mut self, width: u32) -> Self {
self.width = Some(width);
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Link {
pub href: String,
pub target: Option<LinkTarget>,
pub rel: Option<String>,
pub media_type: Option<String>,
pub href_lang: Option<String>,
pub title: Option<String>,
pub length: Option<u64>,
}
impl Link {
pub(crate) fn new<S: AsRef<str>>(href: S, base: Option<&Url>) -> Link {
let href = match util::parse_uri(href.as_ref(), base) {
Some(uri) => uri.to_string(),
None => href.as_ref().to_string(),
}
.trim()
.to_string();
Link {
href,
target: None,
rel: None,
media_type: None,
href_lang: None,
title: None,
length: None,
}
}
}
#[cfg(test)]
impl Link {
pub fn href_lang(mut self, lang: &str) -> Self {
self.href_lang = Some(lang.to_owned());
self
}
pub fn length(mut self, length: u64) -> Self {
self.length = Some(length);
self
}
pub fn media_type(mut self, media_type: &str) -> Self {
self.media_type = Some(media_type.to_owned());
self
}
pub fn rel(mut self, rel: &str) -> Self {
self.rel = Some(rel.to_owned());
self
}
pub fn target(mut self, target: LinkTarget) -> Self {
self.target = Some(target);
self
}
pub fn title(mut self, title: &str) -> Self {
self.title = Some(title.to_owned());
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum LinkTarget {
Comments,
CommentsFeed,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct MediaObject {
pub(crate) source: Option<MediaObjectSource>,
pub title: Option<Text>,
pub content: Vec<MediaContent>,
pub duration: Option<Duration>,
pub thumbnails: Vec<MediaThumbnail>,
pub texts: Vec<MediaText>,
pub description: Option<Text>,
pub community: Option<MediaCommunity>,
pub credits: Vec<MediaCredit>,
pub people: Vec<PodcastPerson>,
pub season: Option<Season>,
pub episode: Option<Episode>,
pub transcripts: Vec<Transcript>,
}
impl MediaObject {
pub(crate) fn is_not_empty(&self) -> bool {
self.title.is_some() || self.description.is_some() || !self.content.is_empty() || !self.texts.is_empty() || !self.transcripts.is_empty()
}
pub(crate) fn new(source: MediaObjectSource) -> Self {
MediaObject {
source: Some(source),
..Default::default()
}
}
}
#[cfg(test)]
impl MediaObject {
pub fn community(mut self, community: MediaCommunity) -> Self {
self.community = Some(community);
self
}
pub fn content(mut self, content: MediaContent) -> Self {
self.content.push(content);
self
}
pub fn credit(mut self, entity: &str) -> Self {
self.credits.push(MediaCredit::new(entity.to_string()));
self
}
pub fn description(mut self, description: &str) -> Self {
self.description = Some(Text::new(description.to_string()));
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
pub fn text(mut self, text: MediaText) -> Self {
self.texts.push(text);
self
}
pub fn thumbnail(mut self, thumbnail: MediaThumbnail) -> Self {
self.thumbnails.push(thumbnail);
self
}
pub fn title(mut self, title: &str) -> Self {
self.title = Some(Text::new(title.to_string()));
self
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub(crate) enum MediaObjectSource {
RSS,
MediaRSS,
ITunes,
Podcast,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct MediaCommunity {
pub stars_avg: Option<f64>,
pub stars_count: Option<u64>,
pub stars_min: Option<u64>,
pub stars_max: Option<u64>,
pub stats_views: Option<u64>,
pub stats_favorites: Option<u64>,
}
impl MediaCommunity {
pub(crate) fn new() -> MediaCommunity {
MediaCommunity {
stars_avg: None,
stars_count: None,
stars_min: None,
stars_max: None,
stats_views: None,
stats_favorites: None,
}
}
}
#[cfg(test)]
impl MediaCommunity {
pub fn star_rating(mut self, count: u64, average: f64, min: u64, max: u64) -> Self {
self.stars_count = Some(count);
self.stars_avg = Some(average);
self.stars_min = Some(min);
self.stars_max = Some(max);
self
}
pub fn statistics(mut self, views: Option<u64>, favorites: Option<u64>) -> Self {
self.stats_views = views;
self.stats_favorites = favorites;
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct MediaContent {
pub url: Option<Url>,
pub content_type: Option<MediaTypeBuf>,
pub height: Option<u32>,
pub width: Option<u32>,
pub duration: Option<Duration>,
pub size: Option<u64>,
pub rating: Option<MediaRating>,
}
#[cfg(test)]
impl MediaContent {
pub fn content_type(mut self, content_type: &str) -> Self {
self.content_type = Some(content_type.parse::<MediaTypeBuf>().unwrap());
self
}
pub fn height(mut self, height: u32) -> Self {
self.height = Some(height);
self
}
pub fn url(mut self, url: &str) -> Self {
self.url = Some(Url::parse(url).unwrap());
self
}
pub fn width(mut self, width: u32) -> Self {
self.width = Some(width);
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
pub fn size(mut self, size: u64) -> Self {
self.size = Some(size);
self
}
pub fn rating(mut self, rating: MediaRating) -> Self {
self.rating = Some(rating);
self
}
}
impl MediaContent {
pub(crate) fn new() -> MediaContent {
MediaContent {
url: None,
content_type: None,
height: None,
width: None,
duration: None,
size: None,
rating: None,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct MediaCredit {
pub entity: String,
}
impl MediaCredit {
pub(crate) fn new(entity: String) -> MediaCredit {
MediaCredit { entity }
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct MediaRating {
pub urn: String,
pub value: String,
}
impl MediaRating {
pub(crate) fn new(value: String) -> MediaRating {
MediaRating { urn: "simple".into(), value }
}
pub fn urn(mut self, urn: &str) -> Self {
self.urn = urn.to_string();
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct MediaText {
pub text: Text,
pub start_time: Option<Duration>,
pub end_time: Option<Duration>,
}
impl MediaText {
pub(crate) fn new(text: Text) -> MediaText {
MediaText {
text,
start_time: None,
end_time: None,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct MediaThumbnail {
pub image: Image,
pub time: Option<Duration>,
}
impl MediaThumbnail {
pub(crate) fn new(image: Image) -> MediaThumbnail {
MediaThumbnail { image, time: None }
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Person {
pub name: Option<String>,
pub uri: Option<String>,
pub email: Option<String>,
pub role: Option<String>,
}
impl Person {
pub(crate) fn empty() -> Person {
Person {
name: None,
uri: None,
email: None,
role: None,
}
}
pub(crate) fn parse(raw: &str) -> Person {
util::parse_person_name_email(raw)
}
pub(crate) fn email(mut self, email: &str) -> Self {
self.email = Some(email.trim().into());
self
}
#[allow(dead_code)]
pub(crate) fn name(mut self, name: &str) -> Self {
self.name = Some(name.trim().into());
self
}
pub(crate) fn role(mut self, role: &str) -> Self {
self.role = Some(role.trim().into());
self
}
}
#[cfg(test)]
impl Person {
pub fn uri(mut self, uri: &str) -> Self {
self.uri = Some(uri.to_owned());
self
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PodcastPerson {
pub name: String,
#[serde(deserialize_with = "podcast_enum")]
pub role: Role,
#[serde(deserialize_with = "podcast_enum")]
pub group: Group,
pub img: Option<Url>,
pub href: Option<Url>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum Role {
Director,
#[serde(rename = "Assistant Director")]
AssistantDirector,
#[serde(rename = "Executive Producer")]
ExecutiveProducer,
#[serde(rename = "Senior Producer")]
SeniorProducer,
Producer,
#[serde(rename = "Associate Producer")]
AssociateProducer,
#[serde(rename = "Development Producer")]
DevelopmentProducer,
#[serde(rename = "Creative Director")]
CreativeDirector,
Host,
#[serde(rename = "Co-Host")]
CoHost,
#[serde(rename = "Guest Host")]
GuestHost,
Guest,
#[serde(rename = "Voice Actor")]
VoiceActor,
Narrator,
Announcer,
Reporter,
Author,
#[serde(rename = "Editorial Director")]
EditorialDirector,
#[serde(rename = "Co-Writer")]
CoWriter,
Writer,
Songwriter,
#[serde(rename = "Guest Writer")]
GuestWriter,
#[serde(rename = "Story Editor")]
StoryEditor,
#[serde(rename = "Managing Editor")]
ManagingEditor,
#[serde(rename = "Script Editor")]
ScriptEditor,
#[serde(rename = "Script Coordinator")]
ScriptCoordinator,
Researcher,
#[serde(rename = "Fact Checker")]
FactChecker,
Translator,
Transcriber,
Logger,
#[serde(rename = "Studio Coordinator")]
StudioCoordinator,
#[serde(rename = "Technical Director")]
TechnicalDirector,
#[serde(rename = "Technical Manager")]
TechnicalManager,
#[serde(rename = "Audio Engineer")]
AudioEngineer,
#[serde(rename = "Remote Recording Engineer")]
RemoteRecordingEngineer,
#[serde(rename = "Post Production Engineer")]
PostProductionEngineer,
#[serde(rename = "Audio Editor")]
AudioEditor,
#[serde(rename = "Sound Designer")]
SoundDesigner,
#[serde(rename = "Foley Artist")]
FoleyArtist,
Composer,
#[serde(rename = "Theme Music")]
ThemeMusic,
#[serde(rename = "Music Production")]
MusicProduction,
#[serde(rename = "Music Contributor")]
MusicContributor,
#[serde(rename = "Production Coordinator")]
ProductionCoordinator,
#[serde(rename = "Booking Coordinator")]
BookingCoordinator,
#[serde(rename = "Production Assistant")]
ProductionAssistant,
#[serde(rename = "Content Manager")]
ContentManager,
#[serde(rename = "Marketing Manager")]
MarketingManager,
#[serde(rename = "Sales Representative")]
SalesRepresentative,
#[serde(rename = "Sales Manager")]
SalesManager,
#[serde(rename = "Graphic Designer")]
GraphicDesigner,
#[serde(rename = "Cover Art Designer")]
CoverArtDesigner,
#[serde(rename = "Social Media Manager")]
SocialMediaManager,
Consultant,
Intern,
#[serde(rename = "Camera Operator")]
CameraOperator,
#[serde(rename = "Lighting Designer")]
LightingDesigner,
#[serde(rename = "Camera Grip")]
CameraGrip,
#[serde(rename = "Assistant Camera")]
AssistantCamera,
Editor,
#[serde(rename = "Assistant Editor")]
AssistantEditor,
}
impl Role {
pub const fn default_group(&self) -> Group {
match self {
Self::Director => Group::CreativeDirection,
Self::AssistantDirector => Group::CreativeDirection,
Self::ExecutiveProducer => Group::CreativeDirection,
Self::SeniorProducer => Group::CreativeDirection,
Self::Producer => Group::CreativeDirection,
Self::AssociateProducer => Group::CreativeDirection,
Self::DevelopmentProducer => Group::CreativeDirection,
Self::CreativeDirector => Group::CreativeDirection,
Self::Host => Group::Cast,
Self::CoHost => Group::Cast,
Self::GuestHost => Group::Cast,
Self::Guest => Group::Cast,
Self::VoiceActor => Group::Cast,
Self::Narrator => Group::Cast,
Self::Announcer => Group::Cast,
Self::Reporter => Group::Cast,
Self::Author => Group::Writing,
Self::EditorialDirector => Group::Writing,
Self::CoWriter => Group::Writing,
Self::Writer => Group::Writing,
Self::Songwriter => Group::Writing,
Self::GuestWriter => Group::Writing,
Self::StoryEditor => Group::Writing,
Self::ManagingEditor => Group::Writing,
Self::ScriptEditor => Group::Writing,
Self::ScriptCoordinator => Group::Writing,
Self::Researcher => Group::Writing,
Self::Editor => Group::Writing,
Self::FactChecker => Group::Writing,
Self::Translator => Group::Writing,
Self::Transcriber => Group::Writing,
Self::Logger => Group::Writing,
Self::StudioCoordinator => Group::AudioProduction,
Self::TechnicalDirector => Group::AudioProduction,
Self::TechnicalManager => Group::AudioProduction,
Self::AudioEngineer => Group::AudioProduction,
Self::RemoteRecordingEngineer => Group::AudioProduction,
Self::PostProductionEngineer => Group::AudioProduction,
Self::AudioEditor => Group::AudioPostProduction,
Self::SoundDesigner => Group::AudioPostProduction,
Self::FoleyArtist => Group::AudioPostProduction,
Self::Composer => Group::AudioPostProduction,
Self::ThemeMusic => Group::AudioPostProduction,
Self::MusicProduction => Group::AudioPostProduction,
Self::MusicContributor => Group::AudioPostProduction,
Self::ProductionCoordinator => Group::Administration,
Self::BookingCoordinator => Group::Administration,
Self::ProductionAssistant => Group::Administration,
Self::ContentManager => Group::Administration,
Self::MarketingManager => Group::Administration,
Self::SalesRepresentative => Group::Administration,
Self::SalesManager => Group::Administration,
Self::GraphicDesigner => Group::Visuals,
Self::CoverArtDesigner => Group::Visuals,
Self::SocialMediaManager => Group::Community,
Self::Consultant => Group::Misc,
Self::Intern => Group::Misc,
Self::CameraOperator => Group::VideoProduction,
Self::LightingDesigner => Group::VideoProduction,
Self::CameraGrip => Group::VideoProduction,
Self::AssistantCamera => Group::VideoProduction,
Self::AssistantEditor => Group::VideoPostProduction,
}
}
pub(crate) fn parse(position: u64, s: &str) -> Result<Self, ParseFeedError> {
match normalise_case(s).as_str() {
"Director" => Ok(Role::Director),
"Assistant Director" => Ok(Role::AssistantDirector),
"Executive Producer" => Ok(Role::ExecutiveProducer),
"Senior Producer" => Ok(Role::SeniorProducer),
"Producer" => Ok(Role::Producer),
"Associate Producer" => Ok(Role::AssociateProducer),
"Development Producer" => Ok(Role::DevelopmentProducer),
"Creative Director" => Ok(Role::CreativeDirector),
"Host" => Ok(Role::Host),
"Co-Host" => Ok(Role::CoHost),
"Guest Host" => Ok(Role::GuestHost),
"Guest" => Ok(Role::Guest),
"Voice Actor" => Ok(Role::VoiceActor),
"Narrator" => Ok(Role::Narrator),
"Announcer" => Ok(Role::Announcer),
"Reporter" => Ok(Role::Reporter),
"Author" => Ok(Role::Author),
"Editorial Director" => Ok(Role::EditorialDirector),
"Co-Writer" => Ok(Role::CoWriter),
"Writer" => Ok(Role::Writer),
"Songwriter" => Ok(Role::Songwriter),
"Guest Writer" => Ok(Role::GuestWriter),
"Story Editor" => Ok(Role::StoryEditor),
"Managing Editor" => Ok(Role::ManagingEditor),
"Script Editor" => Ok(Role::ScriptEditor),
"Script Coordinator" => Ok(Role::ScriptCoordinator),
"Researcher" => Ok(Role::Researcher),
"Fact Checker" => Ok(Role::FactChecker),
"Translator" => Ok(Role::Translator),
"Transcriber" => Ok(Role::Transcriber),
"Logger" => Ok(Role::Logger),
"Studio Coordinator" => Ok(Role::StudioCoordinator),
"Technical Director" => Ok(Role::TechnicalDirector),
"Technical Manager" => Ok(Role::TechnicalManager),
"Audio Engineer" => Ok(Role::AudioEngineer),
"Remote Recording Engineer" => Ok(Role::RemoteRecordingEngineer),
"Post Production Engineer" => Ok(Role::PostProductionEngineer),
"Audio Editor" => Ok(Role::AudioEditor),
"Sound Designer" => Ok(Role::SoundDesigner),
"Foley Artist" => Ok(Role::FoleyArtist),
"Composer" => Ok(Role::Composer),
"Theme Music" => Ok(Role::ThemeMusic),
"Music Production" => Ok(Role::MusicProduction),
"Music Contributor" => Ok(Role::MusicContributor),
"Production Coordinator" => Ok(Role::ProductionCoordinator),
"Booking Coordinator" => Ok(Role::BookingCoordinator),
"Production Assistant" => Ok(Role::ProductionAssistant),
"Content Manager" => Ok(Role::ContentManager),
"Marketing Manager" => Ok(Role::MarketingManager),
"Sales Representative" => Ok(Role::SalesRepresentative),
"Sales Manager" => Ok(Role::SalesManager),
"Graphic Designer" => Ok(Role::GraphicDesigner),
"Cover Art Designer" => Ok(Role::CoverArtDesigner),
"Social Media Manager" => Ok(Role::SocialMediaManager),
"Consultant" => Ok(Role::Consultant),
"Intern" => Ok(Role::Intern),
"Camera Operator" => Ok(Role::CameraOperator),
"Lighting Designer" => Ok(Role::LightingDesigner),
"Camera Grip" => Ok(Role::CameraGrip),
"Assistant Camera" => Ok(Role::AssistantCamera),
"Editor" => Ok(Role::Editor),
"Assistant Editor" => Ok(Role::AssistantEditor),
_ => Err(ParseFeedError::ParseError(ParseErrorKind::UnknownEnumVariant {
position,
reference: s.to_string(),
})),
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum Group {
Administration,
#[serde(rename = "Audio Post-Production")]
AudioPostProduction,
#[serde(rename = "Audio Production")]
AudioProduction,
#[default]
Cast,
Community,
#[serde(rename = "Creative Direction")]
CreativeDirection,
#[serde(rename = "Misc.")]
Misc,
#[serde(rename = "Video Post-Production")]
VideoPostProduction,
#[serde(rename = "Video Production")]
VideoProduction,
Visuals,
Writing,
}
impl Group {
pub(crate) fn parse(position: u64, s: &str) -> Result<Self, ParseFeedError> {
match normalise_case(s).as_str() {
"Administration" => Ok(Group::Administration),
"Audio Post-Production" => Ok(Group::AudioPostProduction),
"Audio Production" => Ok(Group::AudioProduction),
"Cast" => Ok(Group::Cast),
"Community" => Ok(Group::Community),
"Creative Direction" => Ok(Group::CreativeDirection),
"Misc" => Ok(Group::Misc),
"Video Post-Production" => Ok(Group::VideoPostProduction),
"Video Production" => Ok(Group::VideoProduction),
"Visuals" => Ok(Group::Visuals),
"Writing" => Ok(Group::Writing),
_ => Err(ParseFeedError::ParseError(ParseErrorKind::UnknownEnumVariant {
position,
reference: s.to_string(),
})),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Transcript {
pub url: Url,
#[serde(rename = "type")]
pub content_type: MediaTypeBuf,
pub language: Option<String>,
pub rel: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Season {
pub name: Option<String>,
pub number: u32,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Episode {
pub display: Option<String>,
pub number: f32,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Text {
pub content_type: MediaTypeBuf,
pub src: Option<String>,
pub content: String,
}
impl Text {
pub(crate) fn new(content: String) -> Text {
Text {
content_type: MediaTypeBuf::new(names::TEXT, names::PLAIN),
src: None,
content: content.trim().to_string(),
}
}
pub(crate) fn html(content: String) -> Text {
Text {
content_type: MediaTypeBuf::new(names::TEXT, names::HTML),
src: None,
content: content.trim().to_string(),
}
}
pub fn sanitize(&mut self) {
#[cfg(feature = "sanitize")]
{
if self.content_type.as_str() != "text/plain" {
self.content = ammonia::clean(&self.content);
}
}
}
}
#[cfg(test)]
impl Text {
pub fn content_type(mut self, content_type: &str) -> Self {
self.content_type = content_type.parse::<MediaTypeBuf>().unwrap();
self
}
}
pub(crate) fn podcast_enum<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: DeserializeOwned,
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
let value = normalise_case(&value);
T::deserialize(Value::String(value)).map_err(serde::de::Error::custom)
}
pub(crate) fn normalise_case(value: &str) -> String {
let mut out = String::with_capacity(value.len());
let value = value.to_lowercase();
let mut capitalise = true;
for char in value.chars() {
if capitalise {
out.push(char.to_ascii_uppercase());
capitalise = false;
} else {
out.push(char);
}
if char == '-' || char == ' ' {
capitalise = true;
}
}
out
}