1use color_eyre::eyre::Context;
2use serde::Deserialize;
3use serde::Serialize;
4
5use crate::{video::Video, xml_feed::Feed};
6
7#[derive(Serialize, Deserialize, Debug, Clone, derive_builder::Builder)]
8pub struct Playlist {
9 pub id: String,
10 pub title: String,
11 pub author: String,
12 pub channel_id: String,
13 pub url: String,
14 pub published: chrono::DateTime<chrono::Utc>,
15 pub videos: Option<Vec<Video>>,
16}
17
18impl Playlist {
19 pub async fn new(id: &str) -> Self {
20 let uri = format!(
21 "https://www.youtube.com/feeds/videos.xml?playlist_id={}",
22 &id
23 );
24
25 let feed: Feed = Feed::new(&uri)
26 .await
27 .wrap_err("Failed to create Feed from given ID.")
28 .unwrap();
29
30 feed.try_into()
31 .wrap_err("Failed to convert Feed to Playlist.")
32 .unwrap()
33 }
34}
35
36impl TryFrom<Feed> for Playlist {
37 type Error = color_eyre::Report;
38
39 fn try_from(f: Feed) -> Result<Self, Self::Error> {
40 let id = f.playlist_id.ok_or_else(|| {
41 color_eyre::eyre::eyre!("Could not find playlist id for given Playlist.")
42 })?;
43 Ok(Self {
44 id: id.clone(),
45 title: f.title,
46 author: f.author,
47 channel_id: f.channel_id,
48 url: format!("https://www.youtube.com/playlist?list={id}"),
49 published: f.published,
50 videos: f.videos,
51 })
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[tokio::test]
60 async fn test_sinclair_lore_playlist() {
61 let sinclair_lore_va_masq = Playlist::new("PLOIA4n5j7KcYj52DQ9orEBJDA9IqBTB3I").await;
62 assert_eq!(
63 sinclair_lore_va_masq.id,
64 "PLOIA4n5j7KcYj52DQ9orEBJDA9IqBTB3I"
65 );
66 assert_eq!(
67 sinclair_lore_va_masq.url,
68 "https://www.youtube.com/playlist?list=PLOIA4n5j7KcYj52DQ9orEBJDA9IqBTB3I"
69 );
70 assert_eq!(sinclair_lore_va_masq.channel_id, "UCH6IMeS2HVdTJZU4BlN6ODg");
71 assert_eq!(
72 sinclair_lore_va_masq.title,
73 "Vampire the Masquerade ► Down Under by Night | Actual Play"
74 );
75 }
76}