feed_rs/model.rs
1use std::time::Duration;
2
3use chrono::{DateTime, Utc};
4use mediatype::{names, MediaTypeBuf};
5use serde::Serialize;
6use url::Url;
7
8use crate::parser::util;
9#[cfg(test)]
10use crate::parser::util::parse_timestamp_lenient;
11
12/// Combined model for a syndication feed (i.e. RSS1, RSS 2, Atom, JSON Feed)
13///
14/// The model is based on the Atom standard as a start with RSS1+2 mapped on to it e.g.
15/// * Atom
16/// * Feed -> Feed
17/// * Entry -> Entry
18/// * RSS 1 + 2
19/// * Channel -> Feed
20/// * Item -> Entry
21///
22/// [Atom spec]: http://www.atomenabled.org/developers/syndication/
23/// [RSS 2 spec]: https://validator.w3.org/feed/docs/rss2.html
24/// [RSS 1 spec]: https://validator.w3.org/feed/docs/rss1.html
25/// [MediaRSS spec]: https://www.rssboard.org/media-rss
26/// [iTunes podcast spec]: https://help.apple.com/itc/podcasts_connect/#/itcb54353390
27/// [iTunes podcast guide]: https://www.feedforall.com/itune-tutorial-tags.htm
28///
29/// Certain elements are not mapped given their limited utility:
30/// * RSS 2:
31/// * channel - docs (pointer to the spec), cloud (for callbacks), textInput (text box e.g. for search)
32/// * item - comments (link to comments on the article), source (pointer to the channel, but our data model links items to a channel)
33/// * RSS 1:
34/// * channel - rdf:about attribute (pointer to feed), textinput (text box e.g. for search)
35#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
36pub struct Feed {
37 /// Type of this feed (e.g. RSS2, Atom etc)
38 pub feed_type: FeedType,
39 /// A unique identifier for this feed
40 /// * Atom (required): Identifies the feed using a universally unique and permanent URI.
41 /// * RSS doesn't require an ID so it is initialised to the hash of the first link or a UUID if not found
42 pub id: String,
43 /// The title of the feed
44 /// * Atom (required): Contains a human readable title for the feed. Often the same as the title of the associated website. This value should not be blank.
45 /// * RSS 1 + 2 (required) "title": The name of the channel. It's how people refer to your service.
46 /// * JSON Feed: is the name of the feed
47 pub title: Option<Text>,
48 /// The time at which the feed was last modified. If not provided in the source, or invalid, it is `None`.
49 /// * Atom (required): Indicates the last time the feed was modified in a significant way.
50 /// * RSS 2 (optional) "lastBuildDate": The last time the content of the channel changed.
51 pub updated: Option<DateTime<Utc>>,
52
53 /// Atom (recommended): Collection of authors defined at the feed level.
54 /// JSON Feed: specifies the feed author.
55 pub authors: Vec<Person>,
56 /// Description of the feed
57 /// * Atom (optional): Contains a human-readable description or subtitle for the feed (from the subtitle element).
58 /// * RSS 1 + 2 (required): Phrase or sentence describing the channel.
59 /// * JSON Feed: description of the feed
60 pub description: Option<Text>,
61 /// Links to related pages
62 /// * Atom (recommended): Identifies a related Web page.
63 /// * RSS 1 + 2 (required): The URL to the HTML website corresponding to the channel.
64 /// * JSON Feed: the homepage and feed URLs
65 pub links: Vec<Link>,
66
67 /// Structured classification of the feed
68 /// * Atom (optional): Specifies a category that the feed belongs to. A feed may have multiple category elements.
69 /// * RSS 2 (optional) "category": Specify one or more categories that the channel belongs to.
70 pub categories: Vec<Category>,
71 /// People who have contributed to the feed
72 /// * Atom (optional): Names one contributor to the feed. A feed may have multiple contributor elements.
73 /// * RSS 2 (optional) "managingEditor": Email address for person responsible for editorial content.
74 /// * RSS 2 (optional) "webMaster": Email address for person responsible for technical issues relating to channel.
75 pub contributors: Vec<Person>,
76 /// Information on the software used to build the feed
77 /// * Atom (optional): Identifies the software used to generate the feed, for debugging and other purposes.
78 /// * RSS 2 (optional): A string indicating the program used to generate the channel.
79 pub generator: Option<Generator>,
80 /// A small icon
81 /// * Atom (optional): Identifies a small image which provides iconic visual identification for the feed.
82 /// * JSON Feed: is the URL of an image for the feed suitable to be used in a source list.
83 pub icon: Option<Image>,
84 /// RSS 2 (optional): The language the channel is written in.
85 pub language: Option<String>,
86 /// An image used to visually identify the feed
87 /// * Atom (optional): Identifies a larger image which provides visual identification for the feed.
88 /// * RSS 1 + 2 (optional) "image": Specifies a GIF, JPEG or PNG image that can be displayed with the channel.
89 /// * JSON Feed: is the URL of an image for the feed suitable to be used in a timeline
90 pub logo: Option<Image>,
91 /// RSS 2 (optional): The publication date for the content in the channel.
92 pub published: Option<DateTime<Utc>>,
93 /// Rating for the content
94 /// * Populated from the media or itunes namespaces
95 pub rating: Option<MediaRating>,
96 /// Rights restricting content within the feed
97 /// * Atom (optional): Conveys information about rights, e.g. copyrights, held in and over the feed.
98 /// * RSS 2 (optional) "copyright": Copyright notice for content in the channel.
99 pub rights: Option<Text>,
100 /// RSS 2 (optional): It's a number of minutes that indicates how long a channel can be cached before refreshing from the source.
101 pub ttl: Option<u32>,
102
103 /// The individual items within the feed
104 /// * Atom (optional): Individual entries within the feed (e.g. a blog post)
105 /// * RSS 1+2 (optional): Individual items within the channel.
106 pub entries: Vec<Entry>,
107}
108
109impl Feed {
110 pub(crate) fn new(feed_type: FeedType) -> Self {
111 Feed {
112 feed_type,
113 id: "".into(),
114 title: None,
115 updated: None,
116 authors: Vec::new(),
117 description: None,
118 links: Vec::new(),
119 categories: Vec::new(),
120 contributors: Vec::new(),
121 generator: None,
122 icon: None,
123 language: None,
124 logo: None,
125 published: None,
126 rating: None,
127 rights: None,
128 ttl: None,
129 entries: Vec::new(),
130 }
131 }
132}
133
134#[cfg(test)]
135impl Feed {
136 pub fn author(mut self, person: Person) -> Self {
137 self.authors.push(person);
138 self
139 }
140
141 pub fn category(mut self, category: Category) -> Self {
142 self.categories.push(category);
143 self
144 }
145
146 pub fn contributor(mut self, person: Person) -> Self {
147 self.contributors.push(person);
148 self
149 }
150
151 pub fn description(mut self, description: Text) -> Self {
152 self.description = Some(description);
153 self
154 }
155
156 pub fn entry(mut self, entry: Entry) -> Self {
157 self.entries.push(entry);
158 self
159 }
160
161 pub fn generator(mut self, generator: Generator) -> Self {
162 self.generator = Some(generator);
163 self
164 }
165
166 pub fn icon(mut self, image: Image) -> Self {
167 self.icon = Some(image);
168 self
169 }
170
171 pub fn id(mut self, id: &str) -> Self {
172 self.id = id.to_string();
173 self
174 }
175
176 pub fn language(mut self, language: &str) -> Self {
177 self.language = Some(language.to_owned());
178 self
179 }
180
181 pub fn link(mut self, link: Link) -> Self {
182 self.links.push(link);
183 self
184 }
185
186 pub fn logo(mut self, image: Image) -> Self {
187 self.logo = Some(image);
188 self
189 }
190
191 pub fn published(mut self, pub_date: &str) -> Self {
192 self.published = parse_timestamp_lenient(pub_date);
193 self
194 }
195
196 pub fn rights(mut self, rights: Text) -> Self {
197 self.rights = Some(rights);
198 self
199 }
200
201 pub fn title(mut self, title: Text) -> Self {
202 self.title = Some(title);
203 self
204 }
205
206 pub fn ttl(mut self, ttl: u32) -> Self {
207 self.ttl = Some(ttl);
208 self
209 }
210
211 pub fn updated(mut self, updated: Option<DateTime<Utc>>) -> Self {
212 self.updated = updated;
213 self
214 }
215
216 pub fn updated_parsed(mut self, updated: &str) -> Self {
217 self.updated = parse_timestamp_lenient(updated);
218 self
219 }
220}
221
222/// Type of a feed (RSS, Atom etc)
223#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
224pub enum FeedType {
225 Atom,
226 JSON,
227 RSS0,
228 RSS1,
229 RSS2,
230}
231
232/// An item within a feed
233#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
234pub struct Entry {
235 /// A unique identifier for this item with a feed. If not supplied it is initialised to a hash of the first link or a UUID if not available.
236 /// * Atom (required): Identifies the entry using a universally unique and permanent URI.
237 /// * RSS 2 (optional) "guid": A string that uniquely identifies the item.
238 /// * RSS 1: does not specify a unique ID as a separate item, but does suggest the URI should be "the same as the link" so we use a hash of the link if found
239 /// * JSON Feed: is unique for that item for that feed over time.
240 pub id: String,
241 /// Title of this item within the feed
242 /// * Atom, RSS 1(required): Contains a human readable title for the entry.
243 /// * RSS 2 (optional): The title of the item.
244 /// * JSON Feed: The title of the item.
245 pub title: Option<Text>,
246 /// Time at which this item was last modified. If not provided in the source, or invalid, it is `None`.
247 /// * Atom (required): Indicates the last time the entry was modified in a significant way.
248 /// * RSS doesn't specify this field, so we copy it from the entry 'published' field for consistency.
249 /// * JSON Feed: the last modification date of this item
250 pub updated: Option<DateTime<Utc>>,
251
252 /// Authors of this item
253 /// * Atom (recommended): Collection of authors defined at the entry level.
254 /// * RSS 2 (optional): Email address of the author of the item.
255 /// * JSON Feed: the author of the item
256 pub authors: Vec<Person>,
257 /// The content of the item
258 /// * Atom (recommended): Contains or links to the complete content of the entry.
259 /// * RSS 2 (optional) "content:encoded": The HTML form of the content
260 /// * JSON Feed: the html content of the item, or the text content if no html is specified
261 pub content: Option<Content>,
262 /// Links associated with this item
263 /// * Atom (recommended): Identifies a related Web page.
264 /// * RSS 2 (optional): The URL of the item.
265 /// * RSS 1 (required): The item's URL.
266 /// * JSON Feed: the url and external URL for the item is the first items, then each subsequent attachment
267 pub links: Vec<Link>,
268 /// A short summary of the item
269 /// * Atom (recommended): Conveys a short summary, abstract, or excerpt of the entry.
270 /// * RSS 1 (optional): Populated from the RSS namespace 'description' field, or if not present, the Dublin Core namespace 'description' field.
271 /// * RSS 2 (optional): Populated from the RSS namespace 'description' field.
272 /// * JSON Feed: the summary for the item, or the text content if no summary is provided and both text and html content are specified
273 ///
274 /// Warning: Some feeds (especially RSS) use significant whitespace in this field even in cases where it should be considered HTML. Consider rendering this field in a way that preserves whitespace-based formatting such as a double-newline to separate paragraphs.
275 pub summary: Option<Text>,
276
277 /// Structured classification of the item
278 /// * Atom (optional): Specifies a category that the entry belongs to. A feed may have multiple category elements.
279 /// * RSS 2 (optional): Includes the item in one or more categories.
280 /// * JSON Feed: the supplied item tags
281 pub categories: Vec<Category>,
282 /// Atom (optional): Names one contributor to the entry. A feed may have multiple contributor elements.
283 pub contributors: Vec<Person>,
284 /// Time at which this item was first published
285 /// * Atom (optional): Contains the time of the initial creation or first availability of the entry.
286 /// * RSS 2 (optional) "pubDate": Indicates when the item was published.
287 /// * JSON Feed: the date at which the item was published
288 pub published: Option<DateTime<Utc>>,
289 /// Atom (optional): If an entry is copied from one feed into another feed, then this contains the source feed metadata.
290 pub source: Option<String>,
291 /// Atom (optional): Conveys information about rights, e.g. copyrights, held in and over the feed.
292 pub rights: Option<Text>,
293
294 /// Extension for MediaRSS - <https://www.rssboard.org/media-rss>
295 /// A MediaObject will be created in two cases:
296 /// 1) each "media:group" element encountered in the feed
297 /// 2) a default for any other "media:*" elements found at the item level
298 ///
299 /// See the Atom tests for youtube and newscred for examples
300 pub media: Vec<MediaObject>,
301
302 /// Atom (optional): The language specified on the item
303 pub language: Option<String>,
304 /// Atom (optional): The base url specified on the item to resolve any relative
305 /// references found within the scope on the item
306 pub base: Option<String>,
307}
308
309impl Default for Entry {
310 fn default() -> Self {
311 Entry {
312 id: "".into(),
313 title: None,
314 updated: None,
315 authors: Vec::new(),
316 content: None,
317 links: Vec::new(),
318 summary: None,
319 categories: Vec::new(),
320 contributors: Vec::new(),
321 published: None,
322 source: None,
323 rights: None,
324 media: Vec::new(),
325 language: None,
326 base: None,
327 }
328 }
329}
330
331#[cfg(test)]
332impl Entry {
333 pub fn author(mut self, person: Person) -> Self {
334 self.authors.push(person);
335 self
336 }
337
338 pub fn category(mut self, category: Category) -> Self {
339 self.categories.push(category);
340 self
341 }
342
343 pub fn content(mut self, content: Content) -> Self {
344 self.content = Some(content);
345 self
346 }
347
348 pub fn contributor(mut self, person: Person) -> Self {
349 self.contributors.push(person);
350 self
351 }
352
353 pub fn id(mut self, id: &str) -> Self {
354 self.id = id.trim().to_string();
355 self
356 }
357
358 pub fn link(mut self, link: Link) -> Self {
359 self.links.push(link);
360 self
361 }
362
363 pub fn published(mut self, published: &str) -> Self {
364 self.published = parse_timestamp_lenient(published);
365 self
366 }
367
368 pub fn rights(mut self, rights: Text) -> Self {
369 self.rights = Some(rights);
370 self
371 }
372
373 pub fn summary(mut self, summary: Text) -> Self {
374 self.summary = Some(summary);
375 self
376 }
377
378 pub fn title(mut self, title: Text) -> Self {
379 self.title = Some(title);
380 self
381 }
382
383 pub fn updated(mut self, updated: Option<DateTime<Utc>>) -> Self {
384 self.updated = updated;
385 self
386 }
387
388 pub fn updated_parsed(mut self, updated: &str) -> Self {
389 self.updated = parse_timestamp_lenient(updated);
390 self
391 }
392
393 pub fn media(mut self, media: MediaObject) -> Self {
394 self.media.push(media);
395 self
396 }
397
398 pub fn language(mut self, language: &str) -> Self {
399 self.language = Some(language.to_owned());
400 self
401 }
402
403 pub fn base(mut self, url: &str) -> Self {
404 self.base = Some(url.to_owned());
405 self
406 }
407}
408
409/// Represents the category of a feed or entry
410///
411/// [Atom spec]: http://www.atomenabled.org/developers/syndication/#category
412/// [RSS 2 spec]: https://validator.w3.org/feed/docs/rss2.html#ltcategorygtSubelementOfLtitemgt
413#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
414pub struct Category {
415 /// The category as a human readable string
416 /// * Atom (required): Identifies the category.
417 /// * RSS 2: The value of the element is a forward-slash-separated string that identifies a hierarchic location in the indicated taxonomy. Processors may establish conventions for the interpretation of categories.
418 /// * JSON Feed: the value of the tag
419 pub term: String,
420 /// Atom (optional): Identifies the categorization scheme via a URI.
421 pub scheme: Option<String>,
422 /// Atom (optional): Provides a human-readable label for display.
423 pub label: Option<String>,
424 /// Sub-categories (typically from the iTunes namespace i.e. <https://help.apple.com/itc/podcasts_connect/#/itcb54353390>)
425 pub subcategories: Vec<Category>,
426}
427
428impl Category {
429 pub fn new(term: &str) -> Category {
430 Category {
431 term: term.trim().into(),
432 scheme: None,
433 label: None,
434 subcategories: Vec::new(),
435 }
436 }
437}
438
439#[cfg(test)]
440impl Category {
441 pub fn label(mut self, label: &str) -> Self {
442 self.label = Some(label.to_owned());
443 self
444 }
445
446 pub fn scheme(mut self, scheme: &str) -> Self {
447 self.scheme = Some(scheme.to_owned());
448 self
449 }
450}
451
452/// Content, or link to the content, for a given entry.
453///
454/// [Atom spec]: http://www.atomenabled.org/developers/syndication/#contentElement
455/// [RSS 2.0]: https://validator.w3.org/feed/docs/rss2.html#ltenclosuregtSubelementOfLtitemgt
456#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
457pub struct Content {
458 /// Atom
459 /// * If the type attribute ends in +xml or /xml, then an xml document of this type is contained inline.
460 /// * If the type attribute starts with text, then an escaped document of this type is contained inline.
461 /// * Otherwise a base64 encoded document of the indicated media type is contained inline.
462 pub body: Option<String>,
463 /// Type of content
464 /// * Atom: The type attribute is either text, html, xhtml, in which case the content element is defined identically to other text constructs.
465 /// * RSS 2: Type says what its type is, a standard MIME type
466 pub content_type: MediaTypeBuf,
467 /// RSS 2.0: Length of the content in bytes
468 pub length: Option<u64>,
469 /// Source of the content
470 /// * Atom: If the src attribute is present, it represents the URI of where the content can be found. The type attribute, if present, is the media type of the content.
471 /// * RSS 2.0: where the enclosure is located
472 pub src: Option<Link>,
473}
474
475impl Default for Content {
476 fn default() -> Content {
477 Content {
478 body: None,
479 content_type: MediaTypeBuf::new(names::TEXT, names::PLAIN),
480 length: None,
481 src: None,
482 }
483 }
484}
485
486impl Content {
487 pub fn sanitize(&mut self) {
488 // We're dealing with a broader variety of possible content types than
489 // in Text, since the possibility exists that we'll be dealing with a base64-encode
490 // image or the like, so we'll target a correspondingly tighter set: text/html
491 // and application/xhtml+xml.
492 #[cfg(feature = "sanitize")]
493 {
494 let content_type = self.content_type.as_str();
495 if content_type == "text/html" || content_type == "application/xhtml+xml" {
496 if let Some(body) = &self.body {
497 self.body = Some(ammonia::clean(body.as_str()));
498 }
499 }
500 }
501 }
502}
503
504#[cfg(test)]
505impl Content {
506 pub fn body(mut self, body: &str) -> Self {
507 self.body = Some(body.to_owned());
508 self
509 }
510
511 pub fn content_type(mut self, content_type: &str) -> Self {
512 self.content_type = content_type.parse::<MediaTypeBuf>().unwrap();
513 self
514 }
515
516 pub fn length(mut self, length: u64) -> Self {
517 self.length = Some(length);
518 self
519 }
520
521 pub fn src(mut self, url: &str) -> Self {
522 self.src = Some(Link::new(url, None));
523 self
524 }
525}
526
527/// Information on the tools used to generate the feed
528///
529/// Atom: Identifies the software used to generate the feed, for debugging and other purposes.
530#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
531pub struct Generator {
532 /// Atom: Additional data
533 /// RSS 2: A string indicating the program used to generate the channel.
534 pub content: String,
535 /// Atom: Link to the tool
536 pub uri: Option<String>,
537 /// Atom: Tool version
538 pub version: Option<String>,
539}
540
541impl Generator {
542 pub(crate) fn new(content: &str) -> Generator {
543 Generator {
544 uri: None,
545 version: None,
546 content: content.trim().into(),
547 }
548 }
549}
550
551#[cfg(test)]
552impl Generator {
553 pub fn uri(mut self, uri: &str) -> Self {
554 self.uri = Some(uri.to_owned());
555 self
556 }
557
558 pub fn version(mut self, version: &str) -> Self {
559 self.version = Some(version.to_owned());
560 self
561 }
562}
563
564/// Represents a a link to an image.
565///
566/// [Atom spec]: http://www.atomenabled.org/developers/syndication/#optionalFeedElements
567/// [RSS 2 spec]: https://validator.w3.org/feed/docs/rss2.html#ltimagegtSubelementOfLtchannelgt
568/// [RSS 1 spec]: https://validator.w3.org/feed/docs/rss1.html#s5.4
569#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
570pub struct Image {
571 /// Link to the image
572 /// * Atom: The URL to an image or logo
573 /// * RSS 1 + 2: the URL of a GIF, JPEG or PNG image that represents the channel.
574 pub uri: String,
575 /// RSS 1 + 2: describes the image, it's used in the ALT attribute of the HTML <img> tag when the channel is rendered in HTML.
576 pub title: Option<String>,
577 /// RSS 1 + 2: the URL of the site, when the channel is rendered, the image is a link to the site.
578 pub link: Option<Link>,
579
580 /// RSS 2 (optional): width of the image
581 pub width: Option<u32>,
582 /// RSS 2 (optional): height of the image
583 pub height: Option<u32>,
584 /// RSS 2 (optional): contains text that is included in the TITLE attribute of the link formed around the image in the HTML rendering.
585 pub description: Option<String>,
586}
587
588impl Image {
589 pub(crate) fn new(uri: String) -> Image {
590 Image {
591 uri,
592 title: None,
593 link: None,
594 width: None,
595 height: None,
596 description: None,
597 }
598 }
599}
600
601#[cfg(test)]
602impl Image {
603 pub fn description(mut self, description: &str) -> Self {
604 self.description = Some(description.to_owned());
605 self
606 }
607
608 pub fn height(mut self, height: u32) -> Self {
609 self.height = Some(height);
610 self
611 }
612
613 pub fn link(mut self, link: &str) -> Self {
614 self.link = Some(Link::new(link, None));
615 self
616 }
617
618 pub fn title(mut self, title: &str) -> Self {
619 self.title = Some(title.to_owned());
620 self
621 }
622
623 pub fn width(mut self, width: u32) -> Self {
624 self.width = Some(width);
625 self
626 }
627}
628
629/// Represents a link to an associated resource for the feed or entry.
630///
631/// [Atom spec]: http://www.atomenabled.org/developers/syndication/#link
632#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
633pub struct Link {
634 /// Link to additional content
635 /// * Atom: The URI of the referenced resource (typically a Web page).
636 /// * RSS 2: The URL to the HTML website corresponding to the channel or item.
637 /// * JSON Feed: the URI to the attachment, feed etc
638 pub href: String,
639 /// A single link relationship type.
640 pub rel: Option<String>,
641 /// Indicates the media type of the resource.
642 pub media_type: Option<String>,
643 /// Indicates the language of the referenced resource.
644 pub href_lang: Option<String>,
645 /// Human readable information about the link, typically for display purposes.
646 pub title: Option<String>,
647 /// The length of the resource, in bytes.
648 pub length: Option<u64>,
649}
650
651impl Link {
652 pub(crate) fn new<S: AsRef<str>>(href: S, base: Option<&Url>) -> Link {
653 let href = match util::parse_uri(href.as_ref(), base) {
654 Some(uri) => uri.to_string(),
655 None => href.as_ref().to_string(),
656 }
657 .trim()
658 .to_string();
659
660 Link {
661 href,
662 rel: None,
663 media_type: None,
664 href_lang: None,
665 title: None,
666 length: None,
667 }
668 }
669}
670
671#[cfg(test)]
672impl Link {
673 pub fn href_lang(mut self, lang: &str) -> Self {
674 self.href_lang = Some(lang.to_owned());
675 self
676 }
677
678 pub fn length(mut self, length: u64) -> Self {
679 self.length = Some(length);
680 self
681 }
682
683 pub fn media_type(mut self, media_type: &str) -> Self {
684 self.media_type = Some(media_type.to_owned());
685 self
686 }
687
688 pub fn rel(mut self, rel: &str) -> Self {
689 self.rel = Some(rel.to_owned());
690 self
691 }
692
693 pub fn title(mut self, title: &str) -> Self {
694 self.title = Some(title.to_owned());
695 self
696 }
697}
698
699/// The top-level representation of a media object
700/// i.e. combines "media:*" elements from the RSS Media spec such as those under a media:group
701#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
702pub struct MediaObject {
703 /// Title of the object (from the media:title element)
704 pub title: Option<Text>,
705 /// Collection of the media content elements
706 pub content: Vec<MediaContent>,
707 /// Duration of the object
708 pub duration: Option<Duration>,
709 /// Representative images for the object (from media:thumbnail elements)
710 pub thumbnails: Vec<MediaThumbnail>,
711 /// A text transcript, closed captioning or lyrics of the media content.
712 pub texts: Vec<MediaText>,
713 /// Short description of the media object (from the media:description element)
714 pub description: Option<Text>,
715 /// Community info (from the media:community element)
716 pub community: Option<MediaCommunity>,
717 /// Credits
718 pub credits: Vec<MediaCredit>,
719}
720
721impl MediaObject {
722 // Checks if this object has been populated with content
723 pub(crate) fn has_content(&self) -> bool {
724 self.title.is_some() || self.description.is_some() || !self.content.is_empty() || !self.thumbnails.is_empty() || !self.texts.is_empty()
725 }
726}
727
728#[cfg(test)]
729impl MediaObject {
730 pub fn community(mut self, community: MediaCommunity) -> Self {
731 self.community = Some(community);
732 self
733 }
734
735 pub fn content(mut self, content: MediaContent) -> Self {
736 self.content.push(content);
737 self
738 }
739
740 pub fn credit(mut self, entity: &str) -> Self {
741 self.credits.push(MediaCredit::new(entity.to_string()));
742 self
743 }
744
745 pub fn description(mut self, description: &str) -> Self {
746 self.description = Some(Text::new(description.to_string()));
747 self
748 }
749
750 pub fn duration(mut self, duration: Duration) -> Self {
751 self.duration = Some(duration);
752 self
753 }
754
755 pub fn text(mut self, text: MediaText) -> Self {
756 self.texts.push(text);
757 self
758 }
759
760 pub fn thumbnail(mut self, thumbnail: MediaThumbnail) -> Self {
761 self.thumbnails.push(thumbnail);
762 self
763 }
764
765 pub fn title(mut self, title: &str) -> Self {
766 self.title = Some(Text::new(title.to_string()));
767 self
768 }
769}
770
771/// Represents a "media:community" item from the RSS Media spec
772#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
773pub struct MediaCommunity {
774 /// Star rating
775 pub stars_avg: Option<f64>,
776 pub stars_count: Option<u64>,
777 pub stars_min: Option<u64>,
778 pub stars_max: Option<u64>,
779
780 /// Statistics on engagement
781 pub stats_views: Option<u64>,
782 pub stats_favorites: Option<u64>,
783}
784
785impl MediaCommunity {
786 pub(crate) fn new() -> MediaCommunity {
787 MediaCommunity {
788 stars_avg: None,
789 stars_count: None,
790 stars_min: None,
791 stars_max: None,
792 stats_views: None,
793 stats_favorites: None,
794 }
795 }
796}
797
798#[cfg(test)]
799impl MediaCommunity {
800 pub fn star_rating(mut self, count: u64, average: f64, min: u64, max: u64) -> Self {
801 self.stars_count = Some(count);
802 self.stars_avg = Some(average);
803 self.stars_min = Some(min);
804 self.stars_max = Some(max);
805 self
806 }
807
808 pub fn statistics(mut self, views: u64, favorites: u64) -> Self {
809 self.stats_views = Some(views);
810 self.stats_favorites = Some(favorites);
811 self
812 }
813}
814
815/// Represents a "media:content" item from the RSS Media spec
816#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
817pub struct MediaContent {
818 /// The direct URL
819 pub url: Option<Url>,
820 /// Standard MIME type
821 pub content_type: Option<MediaTypeBuf>,
822 /// Height and width
823 pub height: Option<u32>,
824 pub width: Option<u32>,
825 /// Duration the media plays
826 pub duration: Option<Duration>,
827 /// Size of media in bytes
828 pub size: Option<u64>,
829 /// Rating
830 pub rating: Option<MediaRating>,
831}
832
833#[cfg(test)]
834impl MediaContent {
835 pub fn content_type(mut self, content_type: &str) -> Self {
836 self.content_type = Some(content_type.parse::<MediaTypeBuf>().unwrap());
837 self
838 }
839
840 pub fn height(mut self, height: u32) -> Self {
841 self.height = Some(height);
842 self
843 }
844
845 pub fn url(mut self, url: &str) -> Self {
846 self.url = Some(Url::parse(url).unwrap());
847 self
848 }
849
850 pub fn width(mut self, width: u32) -> Self {
851 self.width = Some(width);
852 self
853 }
854
855 pub fn duration(mut self, duration: Duration) -> Self {
856 self.duration = Some(duration);
857 self
858 }
859
860 pub fn size(mut self, size: u64) -> Self {
861 self.size = Some(size);
862 self
863 }
864}
865
866impl MediaContent {
867 pub(crate) fn new() -> MediaContent {
868 MediaContent {
869 url: None,
870 content_type: None,
871 height: None,
872 width: None,
873 duration: None,
874 size: None,
875 rating: None,
876 }
877 }
878}
879
880/// Represents a "media:credit" item from the RSS Media spec
881#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
882pub struct MediaCredit {
883 /// The entity being credited
884 pub entity: String,
885}
886
887impl MediaCredit {
888 pub(crate) fn new(entity: String) -> MediaCredit {
889 MediaCredit { entity }
890 }
891}
892
893/// Rating of the feed, item or media within the content
894#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
895pub struct MediaRating {
896 // The scheme (defaults to "simple" per the spec)
897 pub urn: String,
898 // The rating text
899 pub value: String,
900}
901
902impl MediaRating {
903 pub(crate) fn new(value: String) -> MediaRating {
904 MediaRating { urn: "simple".into(), value }
905 }
906
907 pub fn urn(mut self, urn: &str) -> Self {
908 self.urn = urn.to_string();
909 self
910 }
911}
912
913/// Represents a "media:text" item from the RSS Media spec
914#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
915pub struct MediaText {
916 /// The text
917 pub text: Text,
918 /// The start time offset that the text starts being relevant to the media object.
919 pub start_time: Option<Duration>,
920 /// The end time that the text is relevant. If this attribute is not provided, and a start time is used, it is expected that the end time is either the end of the clip or the start of the next <media:text> element.
921 pub end_time: Option<Duration>,
922}
923
924impl MediaText {
925 pub(crate) fn new(text: Text) -> MediaText {
926 MediaText {
927 text,
928 start_time: None,
929 end_time: None,
930 }
931 }
932}
933
934/// Represents a "media:thumbnail" item from the RSS Media spec
935#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
936pub struct MediaThumbnail {
937 /// The thumbnail image
938 pub image: Image,
939 /// The time this thumbnail represents
940 pub time: Option<Duration>,
941}
942
943impl MediaThumbnail {
944 pub(crate) fn new(image: Image) -> MediaThumbnail {
945 MediaThumbnail { image, time: None }
946 }
947}
948
949/// Represents an author, contributor etc.
950///
951/// [Atom spec]: http://www.atomenabled.org/developers/syndication/#person
952#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
953pub struct Person {
954 /// Atom: human-readable name for the person.
955 /// JSON Feed: human-readable name for the person.
956 pub name: String,
957 /// Atom: home page for the person.
958 /// JSON Feed: link to media (Twitter etc) for the person
959 pub uri: Option<String>,
960 /// Atom: An email address for the person.
961 pub email: Option<String>,
962}
963
964impl Person {
965 pub(crate) fn new(name: &str) -> Person {
966 Person {
967 name: name.trim().into(),
968 uri: None,
969 email: None,
970 }
971 }
972
973 pub fn email(mut self, email: &str) -> Self {
974 self.email = Some(email.to_owned());
975 self
976 }
977}
978
979#[cfg(test)]
980impl Person {
981 pub fn uri(mut self, uri: &str) -> Self {
982 self.uri = Some(uri.to_owned());
983 self
984 }
985}
986
987/// Textual content, or link to the content, for a given entry.
988#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
989pub struct Text {
990 pub content_type: MediaTypeBuf,
991 pub src: Option<String>,
992 pub content: String,
993}
994
995impl Text {
996 pub(crate) fn new(content: String) -> Text {
997 Text {
998 content_type: MediaTypeBuf::new(names::TEXT, names::PLAIN),
999 src: None,
1000 content: content.trim().to_string(),
1001 }
1002 }
1003
1004 pub(crate) fn html(content: String) -> Text {
1005 Text {
1006 content_type: MediaTypeBuf::new(names::TEXT, names::HTML),
1007 src: None,
1008 content: content.trim().to_string(),
1009 }
1010 }
1011
1012 pub fn sanitize(&mut self) {
1013 #[cfg(feature = "sanitize")]
1014 {
1015 if self.content_type.as_str() != "text/plain" {
1016 self.content = ammonia::clean(&self.content);
1017 }
1018 }
1019 }
1020}
1021
1022#[cfg(test)]
1023impl Text {
1024 pub fn content_type(mut self, content_type: &str) -> Self {
1025 self.content_type = content_type.parse::<MediaTypeBuf>().unwrap();
1026 self
1027 }
1028}