1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use super::PrivacyStatus;
use crate::{
    auth::AuthToken,
    common::{PlaylistID, VideoID},
    query::{PostMethod, PostQuery, Query},
};
use serde_json::json;
use std::borrow::Cow;

pub trait CreatePlaylistType {
    fn additional_header(&self) -> Option<(String, serde_json::Value)>;
}

/// A playlist can be created using a list of video ids, or as a copy of an
/// existing playlist (but not both at the same time).
#[derive(Debug, Clone, PartialEq)]
pub struct CreatePlaylistQuery<'a, C: CreatePlaylistType> {
    title: Cow<'a, str>,
    description: Option<Cow<'a, str>>,
    privacy_status: PrivacyStatus,
    query_type: C,
}

/// Helper struct for CreatePlaylistQuery
#[derive(Default, Debug, Clone, PartialEq)]
pub struct BasicCreatePlaylist {}
/// Helper struct for CreatePlaylistQuery
#[derive(Default, Debug, Clone, PartialEq)]
pub struct CreatePlaylistFromVideos<'a> {
    video_ids: Vec<VideoID<'a>>,
}
/// Helper struct for CreatePlaylistQuery
#[derive(Debug, Clone, PartialEq)]
pub struct CreatePlaylistFromPlaylist<'a> {
    source_playlist: PlaylistID<'a>,
}

impl CreatePlaylistType for BasicCreatePlaylist {
    fn additional_header(&self) -> Option<(String, serde_json::Value)> {
        None
    }
}
impl<'a> CreatePlaylistType for CreatePlaylistFromVideos<'a> {
    fn additional_header(&self) -> Option<(String, serde_json::Value)> {
        Some(("videoIds".into(), json!(self.video_ids)))
    }
}
impl<'a> CreatePlaylistType for CreatePlaylistFromPlaylist<'a> {
    fn additional_header(&self) -> Option<(String, serde_json::Value)> {
        Some(("sourcePlaylistId".into(), json!(self.source_playlist)))
    }
}

impl<'a> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
    pub fn new(
        title: &'a str,
        description: Option<&'a str>,
        privacy_status: PrivacyStatus,
    ) -> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
        CreatePlaylistQuery {
            title: title.into(),
            description: description.map(|d| d.into()),
            privacy_status,
            query_type: BasicCreatePlaylist {},
        }
    }
}

impl<'a> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
    pub fn with_source<T: Into<PlaylistID<'a>>>(
        self,
        source_playlist: T,
    ) -> CreatePlaylistQuery<'a, CreatePlaylistFromPlaylist<'a>> {
        let CreatePlaylistQuery {
            title,
            description,
            privacy_status,
            ..
        } = self;
        CreatePlaylistQuery {
            title,
            description,
            privacy_status,
            query_type: CreatePlaylistFromPlaylist {
                source_playlist: source_playlist.into(),
            },
        }
    }
}

impl<'a> CreatePlaylistQuery<'a, BasicCreatePlaylist> {
    pub fn with_video_ids(
        self,
        video_ids: Vec<VideoID<'a>>,
    ) -> CreatePlaylistQuery<'a, CreatePlaylistFromVideos> {
        let CreatePlaylistQuery {
            title,
            description,
            privacy_status,
            ..
        } = self;
        CreatePlaylistQuery {
            title,
            description,
            privacy_status,
            query_type: CreatePlaylistFromVideos { video_ids },
        }
    }
}

impl<'a, A: AuthToken, C: CreatePlaylistType> Query<A> for CreatePlaylistQuery<'a, C> {
    type Output = PlaylistID<'static>;
    type Method = PostMethod;
}
impl<'a, C: CreatePlaylistType> PostQuery for CreatePlaylistQuery<'a, C> {
    fn header(&self) -> serde_json::Map<String, serde_json::Value> {
        // TODO: Confirm if processing required to remove 'VL' portion of playlistId
        let serde_json::Value::Object(mut map) = json!({
            "title" : self.title,
            "privacyStatus" : self.privacy_status.to_string(),
        }) else {
            unreachable!()
        };
        if let Some(description) = &self.description {
            // TODO: Process description to ensure it doesn't contain html. Google doesn't
            // allow html.
            // https://github.com/sigma67/ytmusicapi/blob/main/ytmusicapi/mixins/playlists.py#L311
            map.insert("description".to_string(), description.as_ref().into());
        }
        if let Some(additional_header) = self.query_type.additional_header() {
            map.insert(additional_header.0, additional_header.1);
        }
        map
    }
    fn path(&self) -> &str {
        "playlist/create"
    }
    fn params(&self) -> Option<Cow<str>> {
        None
    }
}