1use std::fmt::Debug;
2use std::time::Duration;
3
4use chrono::{DateTime, Utc};
5use mediatype::{MediaTypeBuf, names};
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Deserializer, Serialize};
8use serde_json::Value;
9use url::Url;
10
11#[cfg(test)]
12use crate::parser::util::parse_timestamp_lenient;
13use crate::parser::{ParseErrorKind, ParseFeedError, util};
14
15#[derive(Clone, Deserialize, PartialEq, Serialize)]
39pub struct Feed {
40 pub feed_type: FeedType,
42 pub id: String,
46 pub title: Option<Text>,
51 pub updated: Option<DateTime<Utc>>,
55
56 pub authors: Vec<Person>,
59 pub description: Option<Text>,
64 pub links: Vec<Link>,
69
70 pub categories: Vec<Category>,
74 pub contributors: Vec<Person>,
79 pub generator: Option<Generator>,
83 pub icon: Option<Image>,
87 pub language: Option<String>,
89 pub logo: Option<Image>,
94 pub published: Option<DateTime<Utc>>,
96 pub rating: Option<MediaRating>,
99 pub rights: Option<Text>,
103 pub ttl: Option<u32>,
105 pub people: Vec<PodcastPerson>,
107
108 pub entries: Vec<Entry>,
112}
113
114impl Feed {
115 pub(crate) fn new(feed_type: FeedType) -> Self {
116 Feed {
117 feed_type,
118 id: "".into(),
119 title: None,
120 updated: None,
121 authors: Vec::new(),
122 description: None,
123 links: Vec::new(),
124 categories: Vec::new(),
125 contributors: Vec::new(),
126 generator: None,
127 icon: None,
128 language: None,
129 logo: None,
130 published: None,
131 rating: None,
132 rights: None,
133 ttl: None,
134 people: Vec::new(),
135 entries: Vec::new(),
136 }
137 }
138}
139
140impl Debug for Feed {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
142 f.write_str(serde_json::to_string(self).unwrap().as_str())
143 }
144}
145
146#[cfg(test)]
147impl Feed {
148 pub fn author(mut self, person: Person) -> Self {
149 self.authors.push(person);
150 self
151 }
152
153 pub fn category(mut self, category: Category) -> Self {
154 self.categories.push(category);
155 self
156 }
157
158 pub fn contributor(mut self, person: Person) -> Self {
159 self.contributors.push(person);
160 self
161 }
162
163 pub fn description(mut self, description: Text) -> Self {
164 self.description = Some(description);
165 self
166 }
167
168 pub fn entry(mut self, entry: Entry) -> Self {
169 self.entries.push(entry);
170 self
171 }
172
173 pub fn generator(mut self, generator: Generator) -> Self {
174 self.generator = Some(generator);
175 self
176 }
177
178 pub fn icon(mut self, image: Image) -> Self {
179 self.icon = Some(image);
180 self
181 }
182
183 pub fn id(mut self, id: &str) -> Self {
184 self.id = id.to_string();
185 self
186 }
187
188 pub fn language(mut self, language: &str) -> Self {
189 self.language = Some(language.to_owned());
190 self
191 }
192
193 pub fn link(mut self, link: Link) -> Self {
194 self.links.push(link);
195 self
196 }
197
198 pub fn logo(mut self, image: Image) -> Self {
199 self.logo = Some(image);
200 self
201 }
202
203 pub fn published(mut self, pub_date: &str) -> Self {
204 self.published = parse_timestamp_lenient(pub_date);
205 self
206 }
207
208 pub fn rights(mut self, rights: Text) -> Self {
209 self.rights = Some(rights);
210 self
211 }
212
213 pub fn title(mut self, title: Text) -> Self {
214 self.title = Some(title);
215 self
216 }
217
218 pub fn ttl(mut self, ttl: u32) -> Self {
219 self.ttl = Some(ttl);
220 self
221 }
222
223 pub fn updated(mut self, updated: Option<DateTime<Utc>>) -> Self {
224 self.updated = updated;
225 self
226 }
227
228 pub fn updated_parsed(mut self, updated: &str) -> Self {
229 self.updated = parse_timestamp_lenient(updated);
230 self
231 }
232}
233
234#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
236pub enum FeedType {
237 Atom,
238 JSON,
239 RSS0,
240 RSS1,
241 RSS2,
242}
243
244#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
246pub struct Entry {
247 pub id: String,
253 pub title: Option<Text>,
258 pub updated: Option<DateTime<Utc>>,
263
264 pub authors: Vec<Person>,
269 pub content: Option<Content>,
274 pub links: Vec<Link>,
280 pub summary: Option<Text>,
288
289 pub categories: Vec<Category>,
294 pub contributors: Vec<Person>,
296 pub published: Option<DateTime<Utc>>,
301 pub source: Option<String>,
303 pub rights: Option<Text>,
305
306 pub media: Vec<MediaObject>,
313
314 pub language: Option<String>,
316 pub base: Option<String>,
319}
320
321impl Default for Entry {
322 fn default() -> Self {
323 Entry {
324 id: "".into(),
325 title: None,
326 updated: None,
327 authors: Vec::new(),
328 content: None,
329 links: Vec::new(),
330 summary: None,
331 categories: Vec::new(),
332 contributors: Vec::new(),
333 published: None,
334 source: None,
335 rights: None,
336 media: Vec::new(),
337 language: None,
338 base: None,
339 }
340 }
341}
342
343#[cfg(test)]
344impl Entry {
345 pub fn author(mut self, person: Person) -> Self {
346 self.authors.push(person);
347 self
348 }
349
350 pub fn category(mut self, category: Category) -> Self {
351 self.categories.push(category);
352 self
353 }
354
355 pub fn content(mut self, content: Content) -> Self {
356 self.content = Some(content);
357 self
358 }
359
360 pub fn contributor(mut self, person: Person) -> Self {
361 self.contributors.push(person);
362 self
363 }
364
365 pub fn id(mut self, id: &str) -> Self {
366 self.id = id.trim().to_string();
367 self
368 }
369
370 pub fn link(mut self, link: Link) -> Self {
371 self.links.push(link);
372 self
373 }
374
375 pub fn published(mut self, published: &str) -> Self {
376 self.published = parse_timestamp_lenient(published);
377 self
378 }
379
380 pub fn rights(mut self, rights: Text) -> Self {
381 self.rights = Some(rights);
382 self
383 }
384
385 pub fn summary(mut self, summary: Text) -> Self {
386 self.summary = Some(summary);
387 self
388 }
389
390 pub fn title(mut self, title: Text) -> Self {
391 self.title = Some(title);
392 self
393 }
394
395 pub fn updated(mut self, updated: Option<DateTime<Utc>>) -> Self {
396 self.updated = updated;
397 self
398 }
399
400 pub fn updated_parsed(mut self, updated: &str) -> Self {
401 self.updated = parse_timestamp_lenient(updated);
402 self
403 }
404
405 pub fn media(mut self, media: MediaObject) -> Self {
406 self.media.push(media);
407 self
408 }
409
410 pub fn language(mut self, language: &str) -> Self {
411 self.language = Some(language.to_owned());
412 self
413 }
414
415 pub fn base(mut self, url: &str) -> Self {
416 self.base = Some(url.to_owned());
417 self
418 }
419}
420
421#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
426pub struct Category {
427 pub term: String,
432 pub scheme: Option<String>,
434 pub label: Option<String>,
436 pub subcategories: Vec<Category>,
438}
439
440impl Category {
441 pub fn new(term: &str) -> Category {
442 Category {
443 term: term.trim().into(),
444 scheme: None,
445 label: None,
446 subcategories: Vec::new(),
447 }
448 }
449}
450
451#[cfg(test)]
452impl Category {
453 pub fn label(mut self, label: &str) -> Self {
454 self.label = Some(label.to_owned());
455 self
456 }
457
458 pub fn scheme(mut self, scheme: &str) -> Self {
459 self.scheme = Some(scheme.to_owned());
460 self
461 }
462}
463
464#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
469pub struct Content {
470 pub body: Option<String>,
475 pub content_type: MediaTypeBuf,
479 pub length: Option<u64>,
481 pub src: Option<Link>,
485}
486
487impl Default for Content {
488 fn default() -> Content {
489 Content {
490 body: None,
491 content_type: MediaTypeBuf::new(names::TEXT, names::PLAIN),
492 length: None,
493 src: None,
494 }
495 }
496}
497
498impl Content {
499 pub fn sanitize(&mut self) {
500 #[cfg(feature = "sanitize")]
505 {
506 let content_type = self.content_type.as_str();
507 if content_type == "text/html" || content_type == "application/xhtml+xml" {
508 if let Some(body) = &self.body {
509 self.body = Some(ammonia::clean(body.as_str()));
510 }
511 }
512 }
513 }
514}
515
516#[cfg(test)]
517impl Content {
518 pub fn body(mut self, body: &str) -> Self {
519 self.body = Some(body.to_owned());
520 self
521 }
522
523 pub fn content_type(mut self, content_type: &str) -> Self {
524 self.content_type = content_type.parse::<MediaTypeBuf>().unwrap();
525 self
526 }
527
528 pub fn length(mut self, length: u64) -> Self {
529 self.length = Some(length);
530 self
531 }
532
533 pub fn src(mut self, url: &str) -> Self {
534 self.src = Some(Link::new(url, None));
535 self
536 }
537}
538
539#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
543pub struct Generator {
544 pub content: String,
547 pub uri: Option<String>,
549 pub version: Option<String>,
551}
552
553impl Generator {
554 pub(crate) fn new(content: &str) -> Generator {
555 Generator {
556 uri: None,
557 version: None,
558 content: content.trim().into(),
559 }
560 }
561}
562
563#[cfg(test)]
564impl Generator {
565 pub fn uri(mut self, uri: &str) -> Self {
566 self.uri = Some(uri.to_owned());
567 self
568 }
569
570 pub fn version(mut self, version: &str) -> Self {
571 self.version = Some(version.to_owned());
572 self
573 }
574}
575
576#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
582pub struct Image {
583 pub uri: String,
587 pub title: Option<String>,
589 pub link: Option<Link>,
591
592 pub width: Option<u32>,
594 pub height: Option<u32>,
596 pub description: Option<String>,
598}
599
600impl Image {
601 pub(crate) fn new(uri: String) -> Image {
602 Image {
603 uri,
604 title: None,
605 link: None,
606 width: None,
607 height: None,
608 description: None,
609 }
610 }
611}
612
613#[cfg(test)]
614impl Image {
615 pub fn description(mut self, description: &str) -> Self {
616 self.description = Some(description.to_owned());
617 self
618 }
619
620 pub fn height(mut self, height: u32) -> Self {
621 self.height = Some(height);
622 self
623 }
624
625 pub fn link(mut self, link: &str) -> Self {
626 self.link = Some(Link::new(link, None));
627 self
628 }
629
630 pub fn title(mut self, title: &str) -> Self {
631 self.title = Some(title.to_owned());
632 self
633 }
634
635 pub fn width(mut self, width: u32) -> Self {
636 self.width = Some(width);
637 self
638 }
639}
640
641#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
645pub struct Link {
646 pub href: String,
651
652 pub target: Option<LinkTarget>,
654
655 pub rel: Option<String>,
657 pub media_type: Option<String>,
659 pub href_lang: Option<String>,
661 pub title: Option<String>,
663 pub length: Option<u64>,
665}
666
667impl Link {
668 pub(crate) fn new<S: AsRef<str>>(href: S, base: Option<&Url>) -> Link {
669 let href = match util::parse_uri(href.as_ref(), base) {
670 Some(uri) => uri.to_string(),
671 None => href.as_ref().to_string(),
672 }
673 .trim()
674 .to_string();
675
676 Link {
677 href,
678 target: None,
679 rel: None,
680 media_type: None,
681 href_lang: None,
682 title: None,
683 length: None,
684 }
685 }
686}
687
688#[cfg(test)]
689impl Link {
690 pub fn href_lang(mut self, lang: &str) -> Self {
691 self.href_lang = Some(lang.to_owned());
692 self
693 }
694
695 pub fn length(mut self, length: u64) -> Self {
696 self.length = Some(length);
697 self
698 }
699
700 pub fn media_type(mut self, media_type: &str) -> Self {
701 self.media_type = Some(media_type.to_owned());
702 self
703 }
704
705 pub fn rel(mut self, rel: &str) -> Self {
706 self.rel = Some(rel.to_owned());
707 self
708 }
709
710 pub fn target(mut self, target: LinkTarget) -> Self {
711 self.target = Some(target);
712 self
713 }
714
715 pub fn title(mut self, title: &str) -> Self {
716 self.title = Some(title.to_owned());
717 self
718 }
719}
720
721#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
723pub enum LinkTarget {
724 Comments,
727 CommentsFeed,
731}
732
733#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
736pub struct MediaObject {
737 pub(crate) source: Option<MediaObjectSource>,
739
740 pub title: Option<Text>,
742 pub content: Vec<MediaContent>,
744 pub duration: Option<Duration>,
746 pub thumbnails: Vec<MediaThumbnail>,
748 pub texts: Vec<MediaText>,
750 pub description: Option<Text>,
752 pub community: Option<MediaCommunity>,
754 pub credits: Vec<MediaCredit>,
756
757 pub people: Vec<PodcastPerson>,
759 pub season: Option<Season>,
761 pub episode: Option<Episode>,
763 pub transcripts: Vec<Transcript>,
765}
766
767impl MediaObject {
768 pub(crate) fn is_not_empty(&self) -> bool {
770 self.title.is_some() || self.description.is_some() || !self.content.is_empty() || !self.texts.is_empty() || !self.transcripts.is_empty()
771 }
772
773 pub(crate) fn new(source: MediaObjectSource) -> Self {
775 MediaObject {
776 source: Some(source),
777 ..Default::default()
778 }
779 }
780}
781
782#[cfg(test)]
783impl MediaObject {
784 pub fn community(mut self, community: MediaCommunity) -> Self {
785 self.community = Some(community);
786 self
787 }
788
789 pub fn content(mut self, content: MediaContent) -> Self {
790 self.content.push(content);
791 self
792 }
793
794 pub fn credit(mut self, entity: &str) -> Self {
795 self.credits.push(MediaCredit::new(entity.to_string()));
796 self
797 }
798
799 pub fn description(mut self, description: &str) -> Self {
800 self.description = Some(Text::new(description.to_string()));
801 self
802 }
803
804 pub fn duration(mut self, duration: Duration) -> Self {
805 self.duration = Some(duration);
806 self
807 }
808
809 pub fn text(mut self, text: MediaText) -> Self {
810 self.texts.push(text);
811 self
812 }
813
814 pub fn thumbnail(mut self, thumbnail: MediaThumbnail) -> Self {
815 self.thumbnails.push(thumbnail);
816 self
817 }
818
819 pub fn title(mut self, title: &str) -> Self {
820 self.title = Some(Text::new(title.to_string()));
821 self
822 }
823}
824
825#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
826pub(crate) enum MediaObjectSource {
827 RSS,
828 MediaRSS,
829 ITunes,
830 Podcast,
831}
832
833#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
835pub struct MediaCommunity {
836 pub stars_avg: Option<f64>,
838 pub stars_count: Option<u64>,
839 pub stars_min: Option<u64>,
840 pub stars_max: Option<u64>,
841
842 pub stats_views: Option<u64>,
844 pub stats_favorites: Option<u64>,
845}
846
847impl MediaCommunity {
848 pub(crate) fn new() -> MediaCommunity {
849 MediaCommunity {
850 stars_avg: None,
851 stars_count: None,
852 stars_min: None,
853 stars_max: None,
854 stats_views: None,
855 stats_favorites: None,
856 }
857 }
858}
859
860#[cfg(test)]
861impl MediaCommunity {
862 pub fn star_rating(mut self, count: u64, average: f64, min: u64, max: u64) -> Self {
863 self.stars_count = Some(count);
864 self.stars_avg = Some(average);
865 self.stars_min = Some(min);
866 self.stars_max = Some(max);
867 self
868 }
869
870 pub fn statistics(mut self, views: Option<u64>, favorites: Option<u64>) -> Self {
871 self.stats_views = views;
872 self.stats_favorites = favorites;
873 self
874 }
875}
876
877#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
879pub struct MediaContent {
880 pub url: Option<Url>,
882 pub content_type: Option<MediaTypeBuf>,
884 pub height: Option<u32>,
886 pub width: Option<u32>,
887 pub duration: Option<Duration>,
889 pub size: Option<u64>,
891 pub rating: Option<MediaRating>,
893}
894
895#[cfg(test)]
896impl MediaContent {
897 pub fn content_type(mut self, content_type: &str) -> Self {
898 self.content_type = Some(content_type.parse::<MediaTypeBuf>().unwrap());
899 self
900 }
901
902 pub fn height(mut self, height: u32) -> Self {
903 self.height = Some(height);
904 self
905 }
906
907 pub fn url(mut self, url: &str) -> Self {
908 self.url = Some(Url::parse(url).unwrap());
909 self
910 }
911
912 pub fn width(mut self, width: u32) -> Self {
913 self.width = Some(width);
914 self
915 }
916
917 pub fn duration(mut self, duration: Duration) -> Self {
918 self.duration = Some(duration);
919 self
920 }
921
922 pub fn size(mut self, size: u64) -> Self {
923 self.size = Some(size);
924 self
925 }
926
927 pub fn rating(mut self, rating: MediaRating) -> Self {
928 self.rating = Some(rating);
929 self
930 }
931}
932
933impl MediaContent {
934 pub(crate) fn new() -> MediaContent {
935 MediaContent {
936 url: None,
937 content_type: None,
938 height: None,
939 width: None,
940 duration: None,
941 size: None,
942 rating: None,
943 }
944 }
945}
946
947#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
949pub struct MediaCredit {
950 pub entity: String,
952}
953
954impl MediaCredit {
955 pub(crate) fn new(entity: String) -> MediaCredit {
956 MediaCredit { entity }
957 }
958}
959
960#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
962pub struct MediaRating {
963 pub urn: String,
965 pub value: String,
967}
968
969impl MediaRating {
970 pub(crate) fn new(value: String) -> MediaRating {
971 MediaRating { urn: "simple".into(), value }
972 }
973
974 pub fn urn(mut self, urn: &str) -> Self {
975 self.urn = urn.to_string();
976 self
977 }
978}
979
980#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
982pub struct MediaText {
983 pub text: Text,
985 pub start_time: Option<Duration>,
987 pub end_time: Option<Duration>,
989}
990
991impl MediaText {
992 pub(crate) fn new(text: Text) -> MediaText {
993 MediaText {
994 text,
995 start_time: None,
996 end_time: None,
997 }
998 }
999}
1000
1001#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1003pub struct MediaThumbnail {
1004 pub image: Image,
1006 pub time: Option<Duration>,
1008}
1009
1010impl MediaThumbnail {
1011 pub(crate) fn new(image: Image) -> MediaThumbnail {
1012 MediaThumbnail { image, time: None }
1013 }
1014}
1015
1016#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1020pub struct Person {
1021 pub name: Option<String>,
1024 pub uri: Option<String>,
1027 pub email: Option<String>,
1029 pub role: Option<String>,
1031}
1032
1033impl Person {
1034 pub(crate) fn empty() -> Person {
1035 Person {
1036 name: None,
1037 uri: None,
1038 email: None,
1039 role: None,
1040 }
1041 }
1042
1043 pub(crate) fn parse(raw: &str) -> Person {
1044 util::parse_person_name_email(raw)
1045 }
1046
1047 pub(crate) fn email(mut self, email: &str) -> Self {
1048 self.email = Some(email.trim().into());
1049 self
1050 }
1051
1052 #[allow(dead_code)]
1053 pub(crate) fn name(mut self, name: &str) -> Self {
1054 self.name = Some(name.trim().into());
1055 self
1056 }
1057
1058 pub(crate) fn role(mut self, role: &str) -> Self {
1059 self.role = Some(role.trim().into());
1060 self
1061 }
1062}
1063
1064#[cfg(test)]
1065impl Person {
1066 pub fn uri(mut self, uri: &str) -> Self {
1067 self.uri = Some(uri.to_owned());
1068 self
1069 }
1070}
1071
1072#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1076pub struct PodcastPerson {
1077 pub name: String,
1078 #[serde(deserialize_with = "podcast_enum")]
1079 pub role: Role,
1080 #[serde(deserialize_with = "podcast_enum")]
1081 pub group: Group,
1082 pub img: Option<Url>,
1083 pub href: Option<Url>,
1084}
1085
1086#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1091pub enum Role {
1092 Director,
1093 #[serde(rename = "Assistant Director")]
1094 AssistantDirector,
1095 #[serde(rename = "Executive Producer")]
1096 ExecutiveProducer,
1097 #[serde(rename = "Senior Producer")]
1098 SeniorProducer,
1099 Producer,
1100 #[serde(rename = "Associate Producer")]
1101 AssociateProducer,
1102 #[serde(rename = "Development Producer")]
1103 DevelopmentProducer,
1104 #[serde(rename = "Creative Director")]
1105 CreativeDirector,
1106 Host,
1107 #[serde(rename = "Co-Host")]
1108 CoHost,
1109 #[serde(rename = "Guest Host")]
1110 GuestHost,
1111 Guest,
1112 #[serde(rename = "Voice Actor")]
1113 VoiceActor,
1114 Narrator,
1115 Announcer,
1116 Reporter,
1117 Author,
1118 #[serde(rename = "Editorial Director")]
1119 EditorialDirector,
1120 #[serde(rename = "Co-Writer")]
1121 CoWriter,
1122 Writer,
1123 Songwriter,
1124 #[serde(rename = "Guest Writer")]
1125 GuestWriter,
1126 #[serde(rename = "Story Editor")]
1127 StoryEditor,
1128 #[serde(rename = "Managing Editor")]
1129 ManagingEditor,
1130 #[serde(rename = "Script Editor")]
1131 ScriptEditor,
1132 #[serde(rename = "Script Coordinator")]
1133 ScriptCoordinator,
1134 Researcher,
1135 #[serde(rename = "Fact Checker")]
1136 FactChecker,
1137 Translator,
1138 Transcriber,
1139 Logger,
1140 #[serde(rename = "Studio Coordinator")]
1141 StudioCoordinator,
1142 #[serde(rename = "Technical Director")]
1143 TechnicalDirector,
1144 #[serde(rename = "Technical Manager")]
1145 TechnicalManager,
1146 #[serde(rename = "Audio Engineer")]
1147 AudioEngineer,
1148 #[serde(rename = "Remote Recording Engineer")]
1149 RemoteRecordingEngineer,
1150 #[serde(rename = "Post Production Engineer")]
1151 PostProductionEngineer,
1152 #[serde(rename = "Audio Editor")]
1153 AudioEditor,
1154 #[serde(rename = "Sound Designer")]
1155 SoundDesigner,
1156 #[serde(rename = "Foley Artist")]
1157 FoleyArtist,
1158 Composer,
1159 #[serde(rename = "Theme Music")]
1160 ThemeMusic,
1161 #[serde(rename = "Music Production")]
1162 MusicProduction,
1163 #[serde(rename = "Music Contributor")]
1164 MusicContributor,
1165 #[serde(rename = "Production Coordinator")]
1166 ProductionCoordinator,
1167 #[serde(rename = "Booking Coordinator")]
1168 BookingCoordinator,
1169 #[serde(rename = "Production Assistant")]
1170 ProductionAssistant,
1171 #[serde(rename = "Content Manager")]
1172 ContentManager,
1173 #[serde(rename = "Marketing Manager")]
1174 MarketingManager,
1175 #[serde(rename = "Sales Representative")]
1176 SalesRepresentative,
1177 #[serde(rename = "Sales Manager")]
1178 SalesManager,
1179 #[serde(rename = "Graphic Designer")]
1180 GraphicDesigner,
1181 #[serde(rename = "Cover Art Designer")]
1182 CoverArtDesigner,
1183 #[serde(rename = "Social Media Manager")]
1184 SocialMediaManager,
1185 Consultant,
1186 Intern,
1187 #[serde(rename = "Camera Operator")]
1188 CameraOperator,
1189 #[serde(rename = "Lighting Designer")]
1190 LightingDesigner,
1191 #[serde(rename = "Camera Grip")]
1192 CameraGrip,
1193 #[serde(rename = "Assistant Camera")]
1194 AssistantCamera,
1195 Editor,
1196 #[serde(rename = "Assistant Editor")]
1197 AssistantEditor,
1198}
1199
1200impl Role {
1201 pub const fn default_group(&self) -> Group {
1202 match self {
1203 Self::Director => Group::CreativeDirection,
1204 Self::AssistantDirector => Group::CreativeDirection,
1205 Self::ExecutiveProducer => Group::CreativeDirection,
1206 Self::SeniorProducer => Group::CreativeDirection,
1207 Self::Producer => Group::CreativeDirection,
1208 Self::AssociateProducer => Group::CreativeDirection,
1209 Self::DevelopmentProducer => Group::CreativeDirection,
1210 Self::CreativeDirector => Group::CreativeDirection,
1211 Self::Host => Group::Cast,
1212 Self::CoHost => Group::Cast,
1213 Self::GuestHost => Group::Cast,
1214 Self::Guest => Group::Cast,
1215 Self::VoiceActor => Group::Cast,
1216 Self::Narrator => Group::Cast,
1217 Self::Announcer => Group::Cast,
1218 Self::Reporter => Group::Cast,
1219 Self::Author => Group::Writing,
1220 Self::EditorialDirector => Group::Writing,
1221 Self::CoWriter => Group::Writing,
1222 Self::Writer => Group::Writing,
1223 Self::Songwriter => Group::Writing,
1224 Self::GuestWriter => Group::Writing,
1225 Self::StoryEditor => Group::Writing,
1226 Self::ManagingEditor => Group::Writing,
1227 Self::ScriptEditor => Group::Writing,
1228 Self::ScriptCoordinator => Group::Writing,
1229 Self::Researcher => Group::Writing,
1230 Self::Editor => Group::Writing,
1231 Self::FactChecker => Group::Writing,
1232 Self::Translator => Group::Writing,
1233 Self::Transcriber => Group::Writing,
1234 Self::Logger => Group::Writing,
1235 Self::StudioCoordinator => Group::AudioProduction,
1236 Self::TechnicalDirector => Group::AudioProduction,
1237 Self::TechnicalManager => Group::AudioProduction,
1238 Self::AudioEngineer => Group::AudioProduction,
1239 Self::RemoteRecordingEngineer => Group::AudioProduction,
1240 Self::PostProductionEngineer => Group::AudioProduction,
1241 Self::AudioEditor => Group::AudioPostProduction,
1242 Self::SoundDesigner => Group::AudioPostProduction,
1243 Self::FoleyArtist => Group::AudioPostProduction,
1244 Self::Composer => Group::AudioPostProduction,
1245 Self::ThemeMusic => Group::AudioPostProduction,
1246 Self::MusicProduction => Group::AudioPostProduction,
1247 Self::MusicContributor => Group::AudioPostProduction,
1248 Self::ProductionCoordinator => Group::Administration,
1249 Self::BookingCoordinator => Group::Administration,
1250 Self::ProductionAssistant => Group::Administration,
1251 Self::ContentManager => Group::Administration,
1252 Self::MarketingManager => Group::Administration,
1253 Self::SalesRepresentative => Group::Administration,
1254 Self::SalesManager => Group::Administration,
1255 Self::GraphicDesigner => Group::Visuals,
1256 Self::CoverArtDesigner => Group::Visuals,
1257 Self::SocialMediaManager => Group::Community,
1258 Self::Consultant => Group::Misc,
1259 Self::Intern => Group::Misc,
1260 Self::CameraOperator => Group::VideoProduction,
1261 Self::LightingDesigner => Group::VideoProduction,
1262 Self::CameraGrip => Group::VideoProduction,
1263 Self::AssistantCamera => Group::VideoProduction,
1264 Self::AssistantEditor => Group::VideoPostProduction,
1265 }
1266 }
1267
1268 pub(crate) fn parse(position: u64, s: &str) -> Result<Self, ParseFeedError> {
1269 match normalise_case(s).as_str() {
1270 "Director" => Ok(Role::Director),
1271 "Assistant Director" => Ok(Role::AssistantDirector),
1272 "Executive Producer" => Ok(Role::ExecutiveProducer),
1273 "Senior Producer" => Ok(Role::SeniorProducer),
1274 "Producer" => Ok(Role::Producer),
1275 "Associate Producer" => Ok(Role::AssociateProducer),
1276 "Development Producer" => Ok(Role::DevelopmentProducer),
1277 "Creative Director" => Ok(Role::CreativeDirector),
1278 "Host" => Ok(Role::Host),
1279 "Co-Host" => Ok(Role::CoHost),
1280 "Guest Host" => Ok(Role::GuestHost),
1281 "Guest" => Ok(Role::Guest),
1282 "Voice Actor" => Ok(Role::VoiceActor),
1283 "Narrator" => Ok(Role::Narrator),
1284 "Announcer" => Ok(Role::Announcer),
1285 "Reporter" => Ok(Role::Reporter),
1286 "Author" => Ok(Role::Author),
1287 "Editorial Director" => Ok(Role::EditorialDirector),
1288 "Co-Writer" => Ok(Role::CoWriter),
1289 "Writer" => Ok(Role::Writer),
1290 "Songwriter" => Ok(Role::Songwriter),
1291 "Guest Writer" => Ok(Role::GuestWriter),
1292 "Story Editor" => Ok(Role::StoryEditor),
1293 "Managing Editor" => Ok(Role::ManagingEditor),
1294 "Script Editor" => Ok(Role::ScriptEditor),
1295 "Script Coordinator" => Ok(Role::ScriptCoordinator),
1296 "Researcher" => Ok(Role::Researcher),
1297 "Fact Checker" => Ok(Role::FactChecker),
1298 "Translator" => Ok(Role::Translator),
1299 "Transcriber" => Ok(Role::Transcriber),
1300 "Logger" => Ok(Role::Logger),
1301 "Studio Coordinator" => Ok(Role::StudioCoordinator),
1302 "Technical Director" => Ok(Role::TechnicalDirector),
1303 "Technical Manager" => Ok(Role::TechnicalManager),
1304 "Audio Engineer" => Ok(Role::AudioEngineer),
1305 "Remote Recording Engineer" => Ok(Role::RemoteRecordingEngineer),
1306 "Post Production Engineer" => Ok(Role::PostProductionEngineer),
1307 "Audio Editor" => Ok(Role::AudioEditor),
1308 "Sound Designer" => Ok(Role::SoundDesigner),
1309 "Foley Artist" => Ok(Role::FoleyArtist),
1310 "Composer" => Ok(Role::Composer),
1311 "Theme Music" => Ok(Role::ThemeMusic),
1312 "Music Production" => Ok(Role::MusicProduction),
1313 "Music Contributor" => Ok(Role::MusicContributor),
1314 "Production Coordinator" => Ok(Role::ProductionCoordinator),
1315 "Booking Coordinator" => Ok(Role::BookingCoordinator),
1316 "Production Assistant" => Ok(Role::ProductionAssistant),
1317 "Content Manager" => Ok(Role::ContentManager),
1318 "Marketing Manager" => Ok(Role::MarketingManager),
1319 "Sales Representative" => Ok(Role::SalesRepresentative),
1320 "Sales Manager" => Ok(Role::SalesManager),
1321 "Graphic Designer" => Ok(Role::GraphicDesigner),
1322 "Cover Art Designer" => Ok(Role::CoverArtDesigner),
1323 "Social Media Manager" => Ok(Role::SocialMediaManager),
1324 "Consultant" => Ok(Role::Consultant),
1325 "Intern" => Ok(Role::Intern),
1326 "Camera Operator" => Ok(Role::CameraOperator),
1327 "Lighting Designer" => Ok(Role::LightingDesigner),
1328 "Camera Grip" => Ok(Role::CameraGrip),
1329 "Assistant Camera" => Ok(Role::AssistantCamera),
1330 "Editor" => Ok(Role::Editor),
1331 "Assistant Editor" => Ok(Role::AssistantEditor),
1332 _ => Err(ParseFeedError::ParseError(ParseErrorKind::UnknownEnumVariant {
1333 position,
1334 reference: s.to_string(),
1335 })),
1336 }
1337 }
1338}
1339
1340#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1345pub enum Group {
1346 Administration,
1347 #[serde(rename = "Audio Post-Production")]
1348 AudioPostProduction,
1349 #[serde(rename = "Audio Production")]
1350 AudioProduction,
1351 #[default]
1352 Cast,
1353 Community,
1354 #[serde(rename = "Creative Direction")]
1355 CreativeDirection,
1356 #[serde(rename = "Misc.")]
1357 Misc,
1358 #[serde(rename = "Video Post-Production")]
1359 VideoPostProduction,
1360 #[serde(rename = "Video Production")]
1361 VideoProduction,
1362 Visuals,
1363 Writing,
1364}
1365
1366impl Group {
1367 pub(crate) fn parse(position: u64, s: &str) -> Result<Self, ParseFeedError> {
1368 match normalise_case(s).as_str() {
1369 "Administration" => Ok(Group::Administration),
1370 "Audio Post-Production" => Ok(Group::AudioPostProduction),
1371 "Audio Production" => Ok(Group::AudioProduction),
1372 "Cast" => Ok(Group::Cast),
1373 "Community" => Ok(Group::Community),
1374 "Creative Direction" => Ok(Group::CreativeDirection),
1375 "Misc" => Ok(Group::Misc),
1376 "Video Post-Production" => Ok(Group::VideoPostProduction),
1377 "Video Production" => Ok(Group::VideoProduction),
1378 "Visuals" => Ok(Group::Visuals),
1379 "Writing" => Ok(Group::Writing),
1380 _ => Err(ParseFeedError::ParseError(ParseErrorKind::UnknownEnumVariant {
1381 position,
1382 reference: s.to_string(),
1383 })),
1384 }
1385 }
1386}
1387
1388#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1392pub struct Transcript {
1393 pub url: Url,
1394 #[serde(rename = "type")]
1395 pub content_type: MediaTypeBuf,
1396 pub language: Option<String>,
1397 pub rel: Option<String>,
1398}
1399
1400#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1404pub struct Season {
1405 pub name: Option<String>,
1406 pub number: u32,
1407}
1408
1409#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1413pub struct Episode {
1414 pub display: Option<String>,
1415 pub number: f32,
1416}
1417
1418#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1420pub struct Text {
1421 pub content_type: MediaTypeBuf,
1422 pub src: Option<String>,
1423 pub content: String,
1424}
1425
1426impl Text {
1427 pub(crate) fn new(content: String) -> Text {
1428 Text {
1429 content_type: MediaTypeBuf::new(names::TEXT, names::PLAIN),
1430 src: None,
1431 content: content.trim().to_string(),
1432 }
1433 }
1434
1435 pub(crate) fn html(content: String) -> Text {
1436 Text {
1437 content_type: MediaTypeBuf::new(names::TEXT, names::HTML),
1438 src: None,
1439 content: content.trim().to_string(),
1440 }
1441 }
1442
1443 pub fn sanitize(&mut self) {
1444 #[cfg(feature = "sanitize")]
1445 {
1446 if self.content_type.as_str() != "text/plain" {
1447 self.content = ammonia::clean(&self.content);
1448 }
1449 }
1450 }
1451}
1452
1453#[cfg(test)]
1454impl Text {
1455 pub fn content_type(mut self, content_type: &str) -> Self {
1456 self.content_type = content_type.parse::<MediaTypeBuf>().unwrap();
1457 self
1458 }
1459}
1460
1461pub(crate) fn podcast_enum<'de, T, D>(deserializer: D) -> Result<T, D::Error>
1463where
1464 T: DeserializeOwned,
1465 D: Deserializer<'de>,
1466{
1467 let value = String::deserialize(deserializer)?;
1468 let value = normalise_case(&value);
1469
1470 T::deserialize(Value::String(value)).map_err(serde::de::Error::custom)
1471}
1472
1473pub(crate) fn normalise_case(value: &str) -> String {
1475 let mut out = String::with_capacity(value.len());
1476 let value = value.to_lowercase();
1477
1478 let mut capitalise = true;
1479 for char in value.chars() {
1480 if capitalise {
1481 out.push(char.to_ascii_uppercase());
1482 capitalise = false;
1483 } else {
1484 out.push(char);
1485 }
1486
1487 if char == '-' || char == ' ' {
1488 capitalise = true;
1489 }
1490 }
1491
1492 out
1493}