tagesschau/
lib.rs

1#![warn(missing_docs)]
2// only enables the `doc_cfg` feature when
3// the `docsrs` configuration attribute is defined
4#![cfg_attr(docsrs, feature(doc_cfg))]
5#![doc = include_str!("../README.md")]
6
7use reqwest;
8
9use reqwest::StatusCode;
10use serde::{de, Deserialize, Deserializer};
11use std::{
12    cmp::Ordering,
13    collections::{HashMap, HashSet},
14    fmt::{self, Display},
15};
16use time::{serde::rfc3339, Date, OffsetDateTime};
17use url::Url;
18
19const BASE_URL: &str = "https://www.tagesschau.de/api2u/news";
20
21/// The german federal states.
22#[repr(u8)]
23#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24pub enum Region {
25    #[allow(missing_docs)]
26    BadenWürttemberg = 1,
27    #[allow(missing_docs)]
28    Bayern = 2,
29    #[allow(missing_docs)]
30    Berlin = 3,
31    #[allow(missing_docs)]
32    Brandenburg = 4,
33    #[allow(missing_docs)]
34    Bremen = 5,
35    #[allow(missing_docs)]
36    Hamburg = 6,
37    #[allow(missing_docs)]
38    Hessen = 7,
39    #[allow(missing_docs)]
40    MecklenburgVorpommern = 8,
41    #[allow(missing_docs)]
42    Niedersachsen = 9,
43    #[allow(missing_docs)]
44    NordrheinWestfalen = 10,
45    #[allow(missing_docs)]
46    RheinlandPfalz = 11,
47    #[allow(missing_docs)]
48    Saarland = 12,
49    #[allow(missing_docs)]
50    Sachsen = 13,
51    #[allow(missing_docs)]
52    SachsenAnhalt = 14,
53    #[allow(missing_docs)]
54    SchleswigHolstein = 15,
55    #[allow(missing_docs)]
56    Thüringen = 16,
57}
58
59/// Months of the year.
60#[repr(u8)]
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum Month {
63    #[allow(missing_docs)]
64    January = 1,
65    #[allow(missing_docs)]
66    February = 2,
67    #[allow(missing_docs)]
68    March = 3,
69    #[allow(missing_docs)]
70    April = 4,
71    #[allow(missing_docs)]
72    May = 5,
73    #[allow(missing_docs)]
74    June = 6,
75    #[allow(missing_docs)]
76    July = 7,
77    #[allow(missing_docs)]
78    August = 8,
79    #[allow(missing_docs)]
80    September = 9,
81    #[allow(missing_docs)]
82    October = 10,
83    #[allow(missing_docs)]
84    November = 11,
85    #[allow(missing_docs)]
86    December = 12,
87}
88
89impl Month {
90    fn to_time_month(&self) -> time::Month {
91        match self {
92            Month::January => time::Month::January,
93            Month::February => time::Month::February,
94            Month::March => time::Month::March,
95            Month::April => time::Month::April,
96            Month::May => time::Month::May,
97            Month::June => time::Month::June,
98            Month::July => time::Month::July,
99            Month::August => time::Month::August,
100            Month::September => time::Month::September,
101            Month::October => time::Month::October,
102            Month::November => time::Month::November,
103            Month::December => time::Month::December,
104        }
105    }
106
107    fn from_time_month(m: time::Month) -> Self {
108        match m {
109            time::Month::January => Month::January,
110            time::Month::February => Month::February,
111            time::Month::March => Month::March,
112            time::Month::April => Month::April,
113            time::Month::May => Month::May,
114            time::Month::June => Month::June,
115            time::Month::July => Month::July,
116            time::Month::August => Month::August,
117            time::Month::September => Month::September,
118            time::Month::October => Month::October,
119            time::Month::November => Month::November,
120            time::Month::December => Month::December,
121        }
122    }
123}
124
125/// The different available news categorys
126#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone)]
127pub enum Ressort {
128    /// With this option, the ressort will not be specified and all results will be shown.
129    None,
130    /// Only news from Germany.
131    Inland,
132    /// Only news from outside of Germany.
133    Ausland,
134    /// Economic news.
135    Wirtschaft,
136    /// Sports news.
137    Sport,
138    /// Different kinds of videos.
139    Video,
140    /// Investigative journalism.
141    Investigativ,
142    /// Informative news that refutes false reports, explain the background and encourage reflection.
143    Wissen,
144}
145
146impl Display for Ressort {
147    /// Formats the ressort value in a way that is usable by the underlying API.
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self {
150            Ressort::None => f.write_str(""),
151            Ressort::Inland => f.write_str("inland"),
152            Ressort::Ausland => f.write_str("ausland"),
153            Ressort::Wirtschaft => f.write_str("wirtschaft"),
154            Ressort::Sport => f.write_str("sport"),
155            Ressort::Video => f.write_str("video"),
156            Ressort::Investigativ => f.write_str("investigativ"),
157            Ressort::Wissen => f.write_str("wissen"),
158        }
159    }
160}
161
162impl<'de> Deserialize<'de> for Ressort {
163    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164    where
165        D: Deserializer<'de>,
166    {
167        let s = String::deserialize(deserializer)?;
168        match s.as_str() {
169            "" => Ok(Ressort::None),
170            "inland" => Ok(Ressort::Inland),
171            "ausland" => Ok(Ressort::Ausland),
172            "wirtschaft" => Ok(Ressort::Wirtschaft),
173            "sport" => Ok(Ressort::Sport),
174            "video" => Ok(Ressort::Video),
175            "investigativ" => Ok(Ressort::Investigativ),
176            "wissen" => Ok(Ressort::Wissen),
177            _ => Err(de::Error::custom(format!(
178                "String does not contain expected value: {}",
179                s
180            ))),
181        }
182    }
183}
184
185/// A timeframe for which the news should be fetched.
186pub enum Timeframe {
187    /// The current date.
188    Now,
189    /// A specific singular date.
190    Date(TDate),
191    /// A range of dates.
192    DateRange(DateRange),
193}
194
195/// A date format for usage in [`Timeframes`](Timeframe).
196#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
197pub struct TDate {
198    day: u8,
199    month: Month,
200    year: i32,
201}
202
203impl TDate {
204    /// Creates a `TDate` from the year, month, and day.
205    pub fn from_calendar_date(year: i32, month: Month, day: u8) -> Result<Self, Error> {
206        let date = Date::from_calendar_date(year, month.to_time_month(), day)?;
207        Ok(TDate::from_time_date(date))
208    }
209
210    /// Creates a `TDate` from a [Date].
211    pub fn from_time_date(d: Date) -> Self {
212        TDate {
213            day: d.day(),
214            month: Month::from_time_month(d.month()),
215            year: d.year(),
216        }
217    }
218}
219
220impl Display for TDate {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(
223            f,
224            "{}{}{}",
225            self.year % 100,
226            format!("{:0>2}", self.month as u8),
227            format!("{:0>2}", self.day)
228        )
229    }
230}
231
232/// A collection of unique [`TDates`](TDate).
233#[derive(Clone, Debug)]
234pub struct DateRange {
235    dates: HashSet<TDate>,
236}
237
238impl DateRange {
239    /// Generates a `DateRange` by encompassing dates within the range defined by two specified [`TDates`](TDate).
240    pub fn new(start: TDate, end: TDate) -> Result<Self, Error> {
241        let mut dates: Vec<TDate> = Vec::new();
242
243        let mut s = Date::from_calendar_date(start.year, start.month.to_time_month(), start.day)?;
244
245        let e = Date::from_calendar_date(end.year, end.month.to_time_month(), end.day)?;
246
247        while s <= e {
248            dates.push(TDate::from_time_date(s));
249            s = s.next_day().unwrap();
250        }
251
252        Ok(Self {
253            dates: HashSet::from_iter(dates.into_iter()),
254        })
255    }
256
257    /// Creates a `DateRange` from a collection of [`TDates`](TDate).
258    pub fn from_dates(dates: Vec<TDate>) -> Self {
259        Self {
260            dates: HashSet::from_iter(dates.into_iter()),
261        }
262    }
263}
264
265/// A client for the [Tagesschau](https://www.tagesschau.de) `/api2/news` endpoint.
266pub struct TRequestBuilder {
267    ressort: Ressort,
268    regions: HashSet<Region>,
269    timeframe: Timeframe,
270}
271
272impl TRequestBuilder {
273    /// Creates a `TRequestBuilder` with no specified ressort, region and the current date as timeframe.
274    pub fn new() -> Self {
275        Self {
276            ressort: Ressort::None,
277            regions: HashSet::new(),
278            timeframe: Timeframe::Now,
279        }
280    }
281
282    /// Sets an existing `TRequestBuilder`'s selected ressort.
283    pub fn ressort(&mut self, res: Ressort) -> &mut TRequestBuilder {
284        self.ressort = res;
285        self
286    }
287
288    /// Sets an existing `TRequestBuilder`'s selected regions.
289    pub fn regions(&mut self, reg: HashSet<Region>) -> &mut TRequestBuilder {
290        self.regions = reg;
291        self
292    }
293
294    /// Sets an existing `TRequestBuilder`'s selected timeframe.
295    pub fn timeframe(&mut self, timeframe: Timeframe) -> &mut TRequestBuilder {
296        self.timeframe = timeframe;
297        self
298    }
299
300    /// Creates the queryable URL for the `fetch` method.
301    fn prepare_url(&self, date: TDate) -> Result<String, Error> {
302        // TODO - Support multiple ressorts
303        let mut url = Url::parse(BASE_URL)?;
304
305        url.query_pairs_mut().append_pair("date", &date.to_string());
306
307        if !self.regions.is_empty() {
308            let mut r = String::new();
309            for region in &self.regions {
310                r.push_str(&format!("{},", *region as u8));
311            }
312
313            url.query_pairs_mut().append_pair("regions", &r);
314        }
315
316        if self.ressort != Ressort::None {
317            url.query_pairs_mut()
318                .append_pair("ressort", &self.ressort.to_string());
319        }
320
321        Ok(url.to_string())
322    }
323
324    /// Processes the URLs created by `prepare_url`.
325    async fn fetch(&self, date: TDate) -> Result<Articles, Error> {
326        let url = self.prepare_url(date)?;
327
328        let response = reqwest::get(url).await.map_err(|e| Error::BadRequest(e))?;
329
330        let text = match response.status() {
331            StatusCode::OK => response.text().await.map_err(|e| Error::ParsingError(e))?,
332            _ => Err(Error::InvalidResponse(response.status().as_u16()))?,
333        };
334
335        let articles: Articles = serde_json::from_str(&text)?;
336
337        Ok(articles)
338    }
339
340    /// Query all articles that match the parameters currently specified on the `TRequestBuilder` Object in form of [Content].
341    pub async fn get_all_articles(&self) -> Result<Vec<Content>, Error> {
342        let dates: Vec<TDate> = match &self.timeframe {
343            Timeframe::Now => {
344                let now = OffsetDateTime::now_local()?;
345
346                vec![TDate::from_time_date(now.date())]
347            }
348            Timeframe::Date(date) => {
349                vec![*date]
350            }
351            Timeframe::DateRange(date_range) => {
352                Vec::from_iter(date_range.dates.clone().into_iter())
353            }
354        };
355
356        let mut content: Vec<Content> = Vec::new();
357
358        for date in dates {
359            let mut art = self.fetch(date).await?;
360
361            content.append(&mut art.news)
362        }
363
364        content.sort_by(|element, next| {
365            let date_element = match element {
366                Content::TextArticle(t) => t.date,
367                Content::Video(v) => v.date,
368            };
369
370            let date_next = match next {
371                Content::TextArticle(t) => t.date,
372                Content::Video(v) => v.date,
373            };
374
375            if date_element > date_next {
376                Ordering::Greater
377            } else if date_element < date_next {
378                Ordering::Less
379            } else {
380                Ordering::Equal
381            }
382        });
383
384        Ok(content)
385    }
386
387    /// Query only [`TextArticle`] articles that match the parameters currently specified on the `TRequestBuilder` Object.
388    pub async fn get_text_articles(&self) -> Result<Vec<TextArticle>, Error> {
389        let articles = self.get_all_articles().await;
390
391        match articles {
392            Ok(mut a) => {
393                a.retain(|x| x.is_text());
394                let mut t: Vec<TextArticle> = Vec::new();
395
396                for content in a {
397                    t.push(content.to_text().unwrap())
398                }
399
400                Ok(t)
401            }
402            Err(e) => Err(e),
403        }
404    }
405
406    /// Query only [`Videos`](Video) that match the parameters currently specified on the `TRequestBuilder` Object.
407    pub async fn get_video_articles(&self) -> Result<Vec<Video>, Error> {
408        let articles = self.get_all_articles().await;
409
410        match articles {
411            Ok(mut a) => {
412                a.retain(|x| x.is_video());
413                let mut t: Vec<Video> = Vec::new();
414
415                for content in a {
416                    t.push(content.to_video().unwrap())
417                }
418
419                Ok(t)
420            }
421            Err(e) => Err(e),
422        }
423    }
424}
425
426#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
427#[cfg(feature = "blocking")]
428mod blocking;
429
430#[derive(Deserialize, Debug)]
431struct Articles {
432    news: Vec<Content>,
433}
434
435/// A value returned by the [TRequestBuilder] that can be either a text article or a video.
436#[derive(Deserialize, Debug)]
437#[serde(untagged)]
438pub enum Content {
439    #[allow(missing_docs)]
440    TextArticle(TextArticle),
441    #[allow(missing_docs)]
442    Video(Video),
443}
444
445impl Content {
446    /// Checks if the `Content` is a [`TextArticle`].
447    pub fn is_text(&self) -> bool {
448        match self {
449            Content::TextArticle(_) => true,
450            Content::Video(_) => false,
451        }
452    }
453
454    /// Checks if the `Content` is a [`Video`].
455    pub fn is_video(&self) -> bool {
456        match self {
457            Content::TextArticle(_) => false,
458            Content::Video(_) => true,
459        }
460    }
461
462    /// Unpacks a and returns a [`TextArticle`].
463    pub fn to_text(self) -> Result<TextArticle, Error> {
464        match self {
465            Content::TextArticle(text) => Ok(text),
466            Content::Video(_) => Err(Error::ConversionError),
467        }
468    }
469
470    /// Unpacks a and returns a [`Video`].
471    pub fn to_video(self) -> Result<Video, Error> {
472        match self {
473            Content::Video(video) => Ok(video),
474            Content::TextArticle(_) => Err(Error::ConversionError),
475        }
476    }
477}
478
479/// A text article returned by the API.
480#[derive(Deserialize, Debug)]
481pub struct TextArticle {
482    title: String,
483    #[serde(rename(deserialize = "firstSentence"))]
484    first_sentence: String,
485    #[serde(with = "rfc3339")]
486    date: OffsetDateTime,
487    #[serde(rename(deserialize = "detailsweb"))]
488    url: String,
489    tags: Option<Vec<Tag>>,
490    ressort: Option<Ressort>,
491    #[serde(rename(deserialize = "type"))]
492    kind: String,
493    #[serde(rename(deserialize = "breakingNews"))]
494    breaking_news: Option<bool>,
495    #[serde(rename(deserialize = "teaserImage"))]
496    image: Option<Image>,
497}
498
499impl TextArticle {
500    /// Get the title of this `TextArticle`.
501    pub fn title(&self) -> &str {
502        &self.title
503    }
504
505    /// Get the first sentence of this `TextArticle`.
506    pub fn first_sentence(&self) -> &str {
507        &self.first_sentence
508    }
509
510    /// Get the publishing time of this `TextArticle` as [OffsetDateTime].
511    pub fn date(&self) -> OffsetDateTime {
512        self.date
513    }
514
515    /// Get the URL to this `TextArticle`.
516    pub fn url(&self) -> &str {
517        &self.url
518    }
519
520    /// Get the tags of this `TextArticle`.
521    pub fn tags(&self) -> Option<Vec<&str>> {
522        match &self.tags {
523            Some(t) => {
524                let mut tags: Vec<&str> = Vec::new();
525                for tag in t {
526                    tags.push(&tag.tag)
527                }
528                Some(tags)
529            }
530            None => None,
531        }
532    }
533
534    /// Get the [`Ressort`] of this `TextArticle`.
535    pub fn ressort(&self) -> Option<Ressort> {
536        self.ressort
537    }
538
539    /// Get the type of `TextArticle` this is.
540    pub fn kind(&self) -> &str {
541        &self.kind
542    }
543
544    /// Get if this `TextArticle` is breaking news or not.
545    pub fn breaking_news(&self) -> Option<bool> {
546        self.breaking_news
547    }
548
549    /// Get the image attached to this `TextArticle`.
550    pub fn image(&self) -> Option<&Image> {
551        self.image.as_ref()
552    }
553}
554
555/// A video returned by the API.
556#[derive(Deserialize, Debug)]
557pub struct Video {
558    title: String,
559    #[serde(with = "rfc3339")]
560    date: OffsetDateTime,
561    streams: HashMap<String, String>,
562    tags: Option<Vec<Tag>>,
563    ressort: Option<Ressort>,
564    #[serde(rename(deserialize = "type"))]
565    kind: String,
566    #[serde(rename(deserialize = "breakingNews"))]
567    breaking_news: Option<bool>,
568    #[serde(rename(deserialize = "teaserImage"))]
569    image: Option<Image>,
570}
571
572impl Video {
573    /// Get the title of this `Video`.
574    pub fn title(&self) -> &str {
575        &self.title
576    }
577
578    /// Get the publishing time of this `Video` as [OffsetDateTime].
579    pub fn date(&self) -> OffsetDateTime {
580        self.date
581    }
582
583    /// Get the [`HashMap`] consisting of (stream-type, URL) (key, value) pairs of this `Video`.
584    pub fn streams(&self) -> HashMap<&str, &str> {
585        let mut streams: HashMap<&str, &str> = HashMap::new();
586        for (key, value) in &self.streams {
587            streams.insert(&key, &value);
588        }
589        streams
590    }
591
592    /// Get the tags of this `Video`.
593    pub fn tags(&self) -> Option<Vec<&str>> {
594        match &self.tags {
595            Some(t) => {
596                let mut tags: Vec<&str> = Vec::new();
597                for tag in t {
598                    tags.push(&tag.tag)
599                }
600                Some(tags)
601            }
602            None => None,
603        }
604    }
605
606    /// Get the [`Ressort`] of this `Video`.
607    pub fn ressort(&self) -> Option<Ressort> {
608        self.ressort
609    }
610
611    /// Get the type of `Video` this is.
612    pub fn kind(&self) -> &str {
613        &self.kind
614    }
615
616    /// Get if this `Video` is breaking news or not.
617    pub fn breaking_news(&self) -> Option<bool> {
618        self.breaking_news
619    }
620
621    /// Get the image attached to this `Video`.
622    pub fn image(&self) -> Option<&Image> {
623        match &self.image {
624            Some(img) => match &img.image_variants {
625                Some(variants) => {
626                    let var = if variants.is_empty() { None } else { Some(img) };
627                    var
628                }
629                None => None,
630            },
631            None => None,
632        };
633        self.image.as_ref()
634    }
635}
636
637#[derive(Deserialize, Debug)]
638struct Tag {
639    tag: String,
640}
641
642/// A struct that contains an images metadata and variants.
643#[derive(Deserialize, Debug, Clone)]
644pub struct Image {
645    title: Option<String>,
646    copyright: Option<String>,
647    alttext: Option<String>,
648    #[serde(rename(deserialize = "imageVariants"))]
649    image_variants: Option<HashMap<String, String>>,
650    #[serde(rename(deserialize = "type"))]
651    kind: String,
652}
653
654impl Image {
655    /// Get the title of this `Image`.
656    pub fn title(&self) -> Option<&str> {
657        match &self.title {
658            Some(title) => Some(&title),
659            None => None,
660        }
661    }
662
663    /// Get the copyright of this `Image`.
664    pub fn copyright(&self) -> Option<&str> {
665        match &self.copyright {
666            Some(copyright) => Some(&copyright),
667            None => None,
668        }
669    }
670
671    /// Get the alt-text of this `Image`.
672    pub fn alttext(&self) -> Option<&str> {
673        match &self.alttext {
674            Some(alttext) => Some(&alttext),
675            None => None,
676        }
677    }
678
679    /// Get the [`HashMap`] consisting of (image-resolution, URL) (key, value) pairs of this `Image`.
680    pub fn image_variants(&self) -> HashMap<&str, &str> {
681        let variants = self.image_variants.as_ref().unwrap();
682        let mut image_variants: HashMap<&str, &str> = HashMap::new();
683        for (key, value) in variants {
684            image_variants.insert(&key, &value);
685        }
686        image_variants
687    }
688
689    /// Get the type of `Image` this is.
690    pub fn kind(&self) -> &str {
691        &self.kind
692    }
693}
694
695/// The Errors that might occur when using the API.
696#[derive(thiserror::Error, Debug)]
697pub enum Error {
698    /// Fetching articles failed.
699    #[error("Fetching articles failed")]
700    BadRequest(reqwest::Error),
701    /// Failed to parse http response.
702    #[error("Failed to parse response")]
703    ParsingError(reqwest::Error),
704    /// Invalid HTTP Response, contains HTTP response code.
705    #[error("Invalid Response: HTTP Response Code {0}")]
706    InvalidResponse(u16),
707    /// Failed to deserialize response.
708    #[error("Failed to deserialize response")]
709    DeserializationError(#[from] serde_json::Error),
710    /// Tried to extract wrong type from [Content].
711    #[error("Tried to extract wrong type")]
712    ConversionError,
713    /// Unable to retrieve current date.
714    #[error("Unable to retrieve current date")]
715    DateError(#[from] time::error::IndeterminateOffset),
716    /// Unable parse date.
717    #[error("Unable parse date")]
718    DateParsingError(#[from] time::error::ComponentRange),
719    /// URL parsing failed.
720    #[error("URL parsing failed")]
721    UrlParsing(#[from] url::ParseError),
722}