use crate::client::{AuthenticatedClient, DeviceClient, PublicClient};
use crate::error::Error;
use crate::subscription::Podcast;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use url::form_urlencoded::byte_serialize;
use url::Url;
#[derive(Deserialize, Serialize, Debug, Clone, Eq)]
pub struct Tag {
pub title: String,
pub tag: String,
pub usage: u16,
}
#[derive(Deserialize, Serialize, Debug, Clone, Eq)]
pub struct Episode {
pub title: String,
pub url: Url,
pub podcast_title: String,
pub podcast_url: Url,
pub description: String,
pub website: Option<Url>,
pub mygpo_link: Url,
pub released: NaiveDateTime,
}
pub trait RetrieveTopTags {
fn retrieve_top_tags(&self, count: u8) -> Result<Vec<Tag>, Error>;
}
pub trait RetrievePodcastsForTag {
fn retrieve_podcasts_for_tag(&self, tag: &str, count: u8) -> Result<Vec<Podcast>, Error>;
}
pub trait RetrievePodcastData {
fn retrieve_podcast_data(&self, url: Url) -> Result<Podcast, Error>;
}
pub trait RetrieveEpisodeData {
fn retrieve_episode_data(&self, podcast: Url, url: Url) -> Result<Episode, Error>;
}
pub trait PodcastToplist {
fn podcast_toplist(&self, number: u8, scale_logo: Option<u16>) -> Result<Vec<Podcast>, Error>;
}
pub trait PodcastSearch {
fn podcast_search(&self, q: &str, scale_logo: Option<u16>) -> Result<Vec<Podcast>, Error>;
}
impl RetrieveTopTags for PublicClient {
fn retrieve_top_tags(&self, count: u8) -> Result<Vec<Tag>, Error> {
Ok(self
.get(&format!(
"https://gpodder.net/api/2/tags/{}.json",
count.to_string()
))?
.json()?)
}
}
impl RetrieveTopTags for AuthenticatedClient {
fn retrieve_top_tags(&self, count: u8) -> Result<Vec<Tag>, Error> {
self.public_client.retrieve_top_tags(count)
}
}
impl RetrieveTopTags for DeviceClient {
fn retrieve_top_tags(&self, count: u8) -> Result<Vec<Tag>, Error> {
self.authenticated_client.retrieve_top_tags(count)
}
}
impl RetrievePodcastsForTag for PublicClient {
fn retrieve_podcasts_for_tag(&self, tag: &str, count: u8) -> Result<Vec<Podcast>, Error> {
let tag_urlencoded: String = byte_serialize(tag.as_bytes()).collect();
Ok(self
.get(&format!(
"https://gpodder.net/api/2/tag/{}/{}.json",
tag_urlencoded,
count.to_string()
))?
.json()?)
}
}
impl RetrievePodcastsForTag for AuthenticatedClient {
fn retrieve_podcasts_for_tag(&self, tag: &str, count: u8) -> Result<Vec<Podcast>, Error> {
self.public_client.retrieve_podcasts_for_tag(tag, count)
}
}
impl RetrievePodcastsForTag for DeviceClient {
fn retrieve_podcasts_for_tag(&self, tag: &str, count: u8) -> Result<Vec<Podcast>, Error> {
self.authenticated_client
.retrieve_podcasts_for_tag(tag, count)
}
}
impl RetrievePodcastData for PublicClient {
fn retrieve_podcast_data(&self, url: Url) -> Result<Podcast, Error> {
Ok(self
.get_with_query(
"https://gpodder.net/api/2/data/podcast.json",
&[&("url", url.as_str())],
)?
.json()?)
}
}
impl RetrievePodcastData for AuthenticatedClient {
fn retrieve_podcast_data(&self, url: Url) -> Result<Podcast, Error> {
self.public_client.retrieve_podcast_data(url)
}
}
impl RetrievePodcastData for DeviceClient {
fn retrieve_podcast_data(&self, url: Url) -> Result<Podcast, Error> {
self.authenticated_client.retrieve_podcast_data(url)
}
}
impl RetrieveEpisodeData for PublicClient {
fn retrieve_episode_data(&self, url: Url, podcast: Url) -> Result<Episode, Error> {
Ok(self
.get_with_query(
"https://gpodder.net/api/2/data/episode.json",
&[&("url", url.as_str()), &("podcast", podcast.as_str())],
)?
.json()?)
}
}
impl RetrieveEpisodeData for AuthenticatedClient {
fn retrieve_episode_data(&self, url: Url, podcast: Url) -> Result<Episode, Error> {
self.public_client.retrieve_episode_data(url, podcast)
}
}
impl RetrieveEpisodeData for DeviceClient {
fn retrieve_episode_data(&self, url: Url, podcast: Url) -> Result<Episode, Error> {
self.authenticated_client
.retrieve_episode_data(url, podcast)
}
}
impl PodcastToplist for PublicClient {
fn podcast_toplist(&self, number: u8, scale_logo: Option<u16>) -> Result<Vec<Podcast>, Error> {
let url = &format!("https://gpodder.net/toplist/{}.json", number);
if let Some(size) = scale_logo {
Ok(self
.get_with_query(url, &[&("scale_logo", size.to_string())])?
.json()?)
} else {
Ok(self.get(url)?.json()?)
}
}
}
impl PodcastToplist for AuthenticatedClient {
fn podcast_toplist(&self, number: u8, scale_logo: Option<u16>) -> Result<Vec<Podcast>, Error> {
self.public_client.podcast_toplist(number, scale_logo)
}
}
impl PodcastToplist for DeviceClient {
fn podcast_toplist(&self, number: u8, scale_logo: Option<u16>) -> Result<Vec<Podcast>, Error> {
self.authenticated_client
.podcast_toplist(number, scale_logo)
}
}
impl PodcastSearch for PublicClient {
fn podcast_search(&self, q: &str, scale_logo: Option<u16>) -> Result<Vec<Podcast>, Error> {
let mut query_parameters: Vec<&(&str, &str)> = Vec::new();
let query_parameter_since = ("q", q);
query_parameters.push(&query_parameter_since);
let scale_logo_string = match scale_logo {
Some(size) => size.to_string(),
None => String::new(),
};
let query_parameter_scale_logo: (&str, &str) = ("scale_logo", scale_logo_string.as_ref());
if !scale_logo_string.is_empty() {
query_parameters.push(&query_parameter_scale_logo);
}
Ok(self
.get_with_query("https://gpodder.net/search.json", &query_parameters)?
.json()?)
}
}
impl PodcastSearch for AuthenticatedClient {
fn podcast_search(&self, q: &str, scale_logo: Option<u16>) -> Result<Vec<Podcast>, Error> {
self.public_client.podcast_search(q, scale_logo)
}
}
impl PodcastSearch for DeviceClient {
fn podcast_search(&self, q: &str, scale_logo: Option<u16>) -> Result<Vec<Podcast>, Error> {
self.authenticated_client.podcast_search(q, scale_logo)
}
}
impl PartialEq for Tag {
fn eq(&self, other: &Self) -> bool {
self.tag == other.tag
}
}
impl Ord for Tag {
fn cmp(&self, other: &Self) -> Ordering {
self.tag.cmp(&other.tag)
}
}
impl PartialOrd for Tag {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for Tag {
fn hash<H: Hasher>(&self, state: &mut H) {
self.tag.hash(state);
}
}
impl fmt::Display for Tag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.tag, self.title)
}
}
impl PartialEq for Episode {
fn eq(&self, other: &Self) -> bool {
self.url == other.url
}
}
impl Ord for Episode {
fn cmp(&self, other: &Self) -> Ordering {
self.url.cmp(&other.url)
}
}
impl PartialOrd for Episode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for Episode {
fn hash<H: Hasher>(&self, state: &mut H) {
self.url.hash(state);
}
}
impl fmt::Display for Episode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.title, self.url)
}
}
#[cfg(test)]
mod tests {
use super::Episode;
use super::Tag;
use chrono::NaiveDate;
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use url::Url;
#[test]
fn equal_tag_means_equal_hash() {
let tag1 = Tag {
title: String::from("TAG"),
tag: String::from("tag"),
usage: 0,
};
let tag2 = Tag {
title: String::from("GAT"),
tag: String::from("tag"),
usage: 100,
};
assert_eq!(tag1, tag2);
assert_eq!(tag1.partial_cmp(&tag2), Some(Ordering::Equal));
let mut hasher1 = DefaultHasher::new();
tag1.hash(&mut hasher1);
let mut hasher2 = DefaultHasher::new();
tag2.hash(&mut hasher2);
assert_eq!(hasher1.finish(), hasher2.finish());
}
#[test]
fn not_equal_tags_have_non_equal_ordering() {
let tag1 = Tag {
title: String::from("TAG"),
tag: String::from("abc"),
usage: 0,
};
let tag2 = Tag {
title: String::from("TAG"),
tag: String::from("xyz"),
usage: 0,
};
assert_ne!(tag1, tag2);
assert_eq!(tag1.partial_cmp(&tag2), Some(Ordering::Less));
let mut hasher1 = DefaultHasher::new();
tag1.hash(&mut hasher1);
let mut hasher2 = DefaultHasher::new();
tag2.hash(&mut hasher2);
assert_ne!(hasher1.finish(), hasher2.finish());
}
#[test]
fn display_tag() {
let tag = Tag {
title: String::from("TAG"),
tag: String::from("xyz"),
usage: 0,
};
assert_eq!("xyz: TAG".to_owned(), format!("{}", tag));
}
#[test]
fn equal_episode_means_equal_hash() {
let episode1 = Episode {
title: String::from("TWiT 245: No Hitler For You"),
url: Url::parse("http://www.podtrac.com/pts/redirect.mp3/aolradio.podcast.aol.com/twit/twit0245.mp3").unwrap(),
podcast_title: String::from("this WEEK in TECH - MP3 Edition"),
podcast_url: Url::parse("http://leo.am/podcasts/twit").unwrap(),
description: String::from("[...]"),
website: Some(Url::parse("http://www.podtrac.com/pts/redirect.mp3/aolradio.podcast.aol.com/twit/twit0245.mp3").unwrap()),
mygpo_link: Url::parse("http://gpodder.net/episode/1046492").unwrap(),
released: NaiveDate::from_ymd(2010, 12, 25).and_hms(0, 30, 0),
};
let episode2 = Episode {
title: String::from("Climate Change, News Corp, and the Australian Fires"),
url: Url::parse("http://www.podtrac.com/pts/redirect.mp3/aolradio.podcast.aol.com/twit/twit0245.mp3").unwrap(),
podcast_title: String::from("On the Media"),
podcast_url: Url::parse("http://feeds.wnyc.org/onthemedia?format=xml").unwrap(),
description: String::from("[...]"),
website: Some(Url::parse("http://www.wnycstudios.org/story/climate-change-news-corp-and-australian-fires/").unwrap()),
mygpo_link: Url::parse("http://gpodder.net/podcast/on-the-media-1/climate-change-news-corp-and-the-australian-fires").unwrap(),
released: NaiveDate::from_ymd(2020, 1, 15).and_hms(17, 0, 0),
};
assert_eq!(episode1, episode2);
assert_eq!(episode1.partial_cmp(&episode2), Some(Ordering::Equal));
let mut hasher1 = DefaultHasher::new();
episode1.hash(&mut hasher1);
let mut hasher2 = DefaultHasher::new();
episode2.hash(&mut hasher2);
assert_eq!(hasher1.finish(), hasher2.finish());
}
#[test]
fn not_equal_episodes_have_non_equal_ordering() {
let episode1 = Episode {
title: String::from("TWiT 245: No Hitler For You"),
url: Url::parse("http://www.podtrac.com/pts/redirect.mp3/aolradio.podcast.aol.com/twit/twit0245.mp3").unwrap(),
podcast_title: String::from("this WEEK in TECH - MP3 Edition"),
podcast_url: Url::parse("http://leo.am/podcasts/twit").unwrap(),
description: String::from("[...]"),
website: Some(Url::parse("http://www.podtrac.com/pts/redirect.mp3/aolradio.podcast.aol.com/twit/twit0245.mp3").unwrap()),
mygpo_link: Url::parse("http://gpodder.net/episode/1046492").unwrap(),
released: NaiveDate::from_ymd(2010, 12, 25).and_hms(0, 30, 0),
};
let episode2 = Episode {
title: String::from("Climate Change, News Corp, and the Australian Fires"),
url: Url::parse("https://www.podtrac.com/pts/redirect.mp3/audio.wnyc.org/otm/otm011520_podextra.mp3").unwrap(),
podcast_title: String::from("On the Media"),
podcast_url: Url::parse("http://feeds.wnyc.org/onthemedia?format=xml").unwrap(),
description: String::from("[...]"),
website: Some(Url::parse("http://www.wnycstudios.org/story/climate-change-news-corp-and-australian-fires/").unwrap()),
mygpo_link: Url::parse("http://gpodder.net/podcast/on-the-media-1/climate-change-news-corp-and-the-australian-fires").unwrap(),
released: NaiveDate::from_ymd(2020, 1, 15).and_hms(17, 0, 0),
};
assert_ne!(episode1, episode2);
assert_eq!(episode1.partial_cmp(&episode2), Some(Ordering::Less));
let mut hasher1 = DefaultHasher::new();
episode1.hash(&mut hasher1);
let mut hasher2 = DefaultHasher::new();
episode2.hash(&mut hasher2);
assert_ne!(hasher1.finish(), hasher2.finish());
}
#[test]
fn display_episode() {
let episode = Episode {
title: String::from("TWiT 245: No Hitler For You"),
url: Url::parse("http://www.podtrac.com/pts/redirect.mp3/aolradio.podcast.aol.com/twit/twit0245.mp3").unwrap(),
podcast_title: String::from("this WEEK in TECH - MP3 Edition"),
podcast_url: Url::parse("http://leo.am/podcasts/twit").unwrap(),
description: String::from("[...]"),
website: Some(Url::parse("http://www.podtrac.com/pts/redirect.mp3/aolradio.podcast.aol.com/twit/twit0245.mp3").unwrap()),
mygpo_link: Url::parse("http://gpodder.net/episode/1046492").unwrap(),
released: NaiveDate::from_ymd(2010, 12, 25).and_hms(0, 30, 0),
};
assert_eq!("TWiT 245: No Hitler For You: http://www.podtrac.com/pts/redirect.mp3/aolradio.podcast.aol.com/twit/twit0245.mp3".to_owned(), format!("{}", episode));
}
}