Skip to main content

ytmapi_rs/query/playlist/
create.rs

1use super::PrivacyStatus;
2use crate::auth::AuthToken;
3use crate::common::{PlaylistID, VideoID};
4use crate::query::{PostMethod, PostQuery, Query};
5use serde_json::json;
6use std::borrow::Cow;
7
8pub trait CreatePlaylistType {
9    fn additional_header(&self) -> Option<(String, serde_json::Value)>;
10}
11
12/// A playlist can be created using a list of video ids, or as a copy of an
13/// existing playlist (but not both at the same time).
14#[derive(Debug, Clone, PartialEq)]
15pub struct CreatePlaylistQuery<'a, C: CreatePlaylistType> {
16    title: Cow<'a, str>,
17    description: Option<Cow<'a, str>>,
18    privacy_status: PrivacyStatus,
19    query_type: C,
20}
21
22/// Helper struct for CreatePlaylistQuery
23#[derive(Default, Debug, Clone, PartialEq)]
24pub struct BasicCreatePlaylist {}
25/// Helper struct for CreatePlaylistQuery
26#[derive(Default, Debug, Clone, PartialEq)]
27pub struct CreatePlaylistFromVideos<'a> {
28    video_ids: Vec<VideoID<'a>>,
29}
30/// Helper struct for CreatePlaylistQuery
31#[derive(Debug, Clone, PartialEq)]
32pub struct CreatePlaylistFromPlaylist<'a> {
33    source_playlist: PlaylistID<'a>,
34}
35
36impl CreatePlaylistType for BasicCreatePlaylist {
37    fn additional_header(&self) -> Option<(String, serde_json::Value)> {
38        None
39    }
40}
41impl CreatePlaylistType for CreatePlaylistFromVideos<'_> {
42    fn additional_header(&self) -> Option<(String, serde_json::Value)> {
43        Some(("videoIds".into(), json!(self.video_ids)))
44    }
45}
46impl CreatePlaylistType for CreatePlaylistFromPlaylist<'_> {
47    fn additional_header(&self) -> Option<(String, serde_json::Value)> {
48        Some(("sourcePlaylistId".into(), json!(self.source_playlist)))
49    }
50}
51
52impl<'a> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
53    pub fn new(
54        title: &'a str,
55        description: Option<&'a str>,
56        privacy_status: PrivacyStatus,
57    ) -> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
58        CreatePlaylistQuery {
59            title: title.into(),
60            description: description.map(|d| d.into()),
61            privacy_status,
62            query_type: BasicCreatePlaylist {},
63        }
64    }
65}
66
67impl<'a> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
68    pub fn with_source<T: Into<PlaylistID<'a>>>(
69        self,
70        source_playlist: T,
71    ) -> CreatePlaylistQuery<'a, CreatePlaylistFromPlaylist<'a>> {
72        let CreatePlaylistQuery {
73            title,
74            description,
75            privacy_status,
76            ..
77        } = self;
78        CreatePlaylistQuery {
79            title,
80            description,
81            privacy_status,
82            query_type: CreatePlaylistFromPlaylist {
83                source_playlist: source_playlist.into(),
84            },
85        }
86    }
87}
88
89impl<'a> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
90    pub fn with_video_ids(
91        self,
92        video_ids: impl IntoIterator<Item = VideoID<'a>>,
93    ) -> CreatePlaylistQuery<'a, CreatePlaylistFromVideos<'a>> {
94        let CreatePlaylistQuery {
95            title,
96            description,
97            privacy_status,
98            ..
99        } = self;
100        CreatePlaylistQuery {
101            title,
102            description,
103            privacy_status,
104            query_type: CreatePlaylistFromVideos {
105                video_ids: video_ids.into_iter().collect(),
106            },
107        }
108    }
109}
110
111impl<A: AuthToken, C: CreatePlaylistType> Query<A> for CreatePlaylistQuery<'_, C> {
112    type Output = PlaylistID<'static>;
113    type Method = PostMethod;
114}
115impl<C: CreatePlaylistType> PostQuery for CreatePlaylistQuery<'_, C> {
116    fn header(&self) -> serde_json::Map<String, serde_json::Value> {
117        // TODO: Confirm if processing required to remove 'VL' portion of playlistId
118        let serde_json::Value::Object(mut map) = json!({
119            "title" : self.title,
120            "privacyStatus" : self.privacy_status.to_string(),
121        }) else {
122            unreachable!()
123        };
124        if let Some(description) = &self.description {
125            // TODO: Process description to ensure it doesn't contain html. Google doesn't
126            // allow html.
127            // https://github.com/sigma67/ytmusicapi/blob/main/ytmusicapi/mixins/playlists.py#L311
128            map.insert("description".to_string(), description.as_ref().into());
129        }
130        if let Some(additional_header) = self.query_type.additional_header() {
131            map.insert(additional_header.0, additional_header.1);
132        }
133        map
134    }
135    fn path(&self) -> &str {
136        "playlist/create"
137    }
138    fn params(&self) -> Vec<(&str, Cow<'_, str>)> {
139        vec![]
140    }
141}