ytmapi_rs/query/playlist/
create.rs

1use super::PrivacyStatus;
2use crate::{
3    auth::AuthToken,
4    common::{PlaylistID, VideoID},
5    query::{PostMethod, PostQuery, Query},
6};
7use serde_json::json;
8use std::borrow::Cow;
9
10pub trait CreatePlaylistType {
11    fn additional_header(&self) -> Option<(String, serde_json::Value)>;
12}
13
14/// A playlist can be created using a list of video ids, or as a copy of an
15/// existing playlist (but not both at the same time).
16#[derive(Debug, Clone, PartialEq)]
17pub struct CreatePlaylistQuery<'a, C: CreatePlaylistType> {
18    title: Cow<'a, str>,
19    description: Option<Cow<'a, str>>,
20    privacy_status: PrivacyStatus,
21    query_type: C,
22}
23
24/// Helper struct for CreatePlaylistQuery
25#[derive(Default, Debug, Clone, PartialEq)]
26pub struct BasicCreatePlaylist {}
27/// Helper struct for CreatePlaylistQuery
28#[derive(Default, Debug, Clone, PartialEq)]
29pub struct CreatePlaylistFromVideos<'a> {
30    video_ids: Vec<VideoID<'a>>,
31}
32/// Helper struct for CreatePlaylistQuery
33#[derive(Debug, Clone, PartialEq)]
34pub struct CreatePlaylistFromPlaylist<'a> {
35    source_playlist: PlaylistID<'a>,
36}
37
38impl CreatePlaylistType for BasicCreatePlaylist {
39    fn additional_header(&self) -> Option<(String, serde_json::Value)> {
40        None
41    }
42}
43impl CreatePlaylistType for CreatePlaylistFromVideos<'_> {
44    fn additional_header(&self) -> Option<(String, serde_json::Value)> {
45        Some(("videoIds".into(), json!(self.video_ids)))
46    }
47}
48impl CreatePlaylistType for CreatePlaylistFromPlaylist<'_> {
49    fn additional_header(&self) -> Option<(String, serde_json::Value)> {
50        Some(("sourcePlaylistId".into(), json!(self.source_playlist)))
51    }
52}
53
54impl<'a> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
55    pub fn new(
56        title: &'a str,
57        description: Option<&'a str>,
58        privacy_status: PrivacyStatus,
59    ) -> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
60        CreatePlaylistQuery {
61            title: title.into(),
62            description: description.map(|d| d.into()),
63            privacy_status,
64            query_type: BasicCreatePlaylist {},
65        }
66    }
67}
68
69impl<'a> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
70    pub fn with_source<T: Into<PlaylistID<'a>>>(
71        self,
72        source_playlist: T,
73    ) -> CreatePlaylistQuery<'a, CreatePlaylistFromPlaylist<'a>> {
74        let CreatePlaylistQuery {
75            title,
76            description,
77            privacy_status,
78            ..
79        } = self;
80        CreatePlaylistQuery {
81            title,
82            description,
83            privacy_status,
84            query_type: CreatePlaylistFromPlaylist {
85                source_playlist: source_playlist.into(),
86            },
87        }
88    }
89}
90
91impl<'a> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
92    pub fn with_video_ids(
93        self,
94        video_ids: Vec<VideoID<'a>>,
95    ) -> CreatePlaylistQuery<'a, CreatePlaylistFromVideos<'a>> {
96        let CreatePlaylistQuery {
97            title,
98            description,
99            privacy_status,
100            ..
101        } = self;
102        CreatePlaylistQuery {
103            title,
104            description,
105            privacy_status,
106            query_type: CreatePlaylistFromVideos { video_ids },
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}