1#![warn(missing_docs)]
2#![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#[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#[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#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone)]
127pub enum Ressort {
128 None,
130 Inland,
132 Ausland,
134 Wirtschaft,
136 Sport,
138 Video,
140 Investigativ,
142 Wissen,
144}
145
146impl Display for Ressort {
147 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
185pub enum Timeframe {
187 Now,
189 Date(TDate),
191 DateRange(DateRange),
193}
194
195#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
197pub struct TDate {
198 day: u8,
199 month: Month,
200 year: i32,
201}
202
203impl TDate {
204 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 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#[derive(Clone, Debug)]
234pub struct DateRange {
235 dates: HashSet<TDate>,
236}
237
238impl DateRange {
239 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 pub fn from_dates(dates: Vec<TDate>) -> Self {
259 Self {
260 dates: HashSet::from_iter(dates.into_iter()),
261 }
262 }
263}
264
265pub struct TRequestBuilder {
267 ressort: Ressort,
268 regions: HashSet<Region>,
269 timeframe: Timeframe,
270}
271
272impl TRequestBuilder {
273 pub fn new() -> Self {
275 Self {
276 ressort: Ressort::None,
277 regions: HashSet::new(),
278 timeframe: Timeframe::Now,
279 }
280 }
281
282 pub fn ressort(&mut self, res: Ressort) -> &mut TRequestBuilder {
284 self.ressort = res;
285 self
286 }
287
288 pub fn regions(&mut self, reg: HashSet<Region>) -> &mut TRequestBuilder {
290 self.regions = reg;
291 self
292 }
293
294 pub fn timeframe(&mut self, timeframe: Timeframe) -> &mut TRequestBuilder {
296 self.timeframe = timeframe;
297 self
298 }
299
300 fn prepare_url(&self, date: TDate) -> Result<String, Error> {
302 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 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 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 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 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#[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 pub fn is_text(&self) -> bool {
448 match self {
449 Content::TextArticle(_) => true,
450 Content::Video(_) => false,
451 }
452 }
453
454 pub fn is_video(&self) -> bool {
456 match self {
457 Content::TextArticle(_) => false,
458 Content::Video(_) => true,
459 }
460 }
461
462 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 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#[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 pub fn title(&self) -> &str {
502 &self.title
503 }
504
505 pub fn first_sentence(&self) -> &str {
507 &self.first_sentence
508 }
509
510 pub fn date(&self) -> OffsetDateTime {
512 self.date
513 }
514
515 pub fn url(&self) -> &str {
517 &self.url
518 }
519
520 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 pub fn ressort(&self) -> Option<Ressort> {
536 self.ressort
537 }
538
539 pub fn kind(&self) -> &str {
541 &self.kind
542 }
543
544 pub fn breaking_news(&self) -> Option<bool> {
546 self.breaking_news
547 }
548
549 pub fn image(&self) -> Option<&Image> {
551 self.image.as_ref()
552 }
553}
554
555#[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 pub fn title(&self) -> &str {
575 &self.title
576 }
577
578 pub fn date(&self) -> OffsetDateTime {
580 self.date
581 }
582
583 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 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 pub fn ressort(&self) -> Option<Ressort> {
608 self.ressort
609 }
610
611 pub fn kind(&self) -> &str {
613 &self.kind
614 }
615
616 pub fn breaking_news(&self) -> Option<bool> {
618 self.breaking_news
619 }
620
621 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#[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 pub fn title(&self) -> Option<&str> {
657 match &self.title {
658 Some(title) => Some(&title),
659 None => None,
660 }
661 }
662
663 pub fn copyright(&self) -> Option<&str> {
665 match &self.copyright {
666 Some(copyright) => Some(©right),
667 None => None,
668 }
669 }
670
671 pub fn alttext(&self) -> Option<&str> {
673 match &self.alttext {
674 Some(alttext) => Some(&alttext),
675 None => None,
676 }
677 }
678
679 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 pub fn kind(&self) -> &str {
691 &self.kind
692 }
693}
694
695#[derive(thiserror::Error, Debug)]
697pub enum Error {
698 #[error("Fetching articles failed")]
700 BadRequest(reqwest::Error),
701 #[error("Failed to parse response")]
703 ParsingError(reqwest::Error),
704 #[error("Invalid Response: HTTP Response Code {0}")]
706 InvalidResponse(u16),
707 #[error("Failed to deserialize response")]
709 DeserializationError(#[from] serde_json::Error),
710 #[error("Tried to extract wrong type")]
712 ConversionError,
713 #[error("Unable to retrieve current date")]
715 DateError(#[from] time::error::IndeterminateOffset),
716 #[error("Unable parse date")]
718 DateParsingError(#[from] time::error::ComponentRange),
719 #[error("URL parsing failed")]
721 UrlParsing(#[from] url::ParseError),
722}