plex_api/myplex/
mod.rs

1pub(crate) mod account;
2pub(crate) mod announcements;
3pub(crate) mod claim_token;
4pub mod device;
5pub mod discover;
6pub(crate) mod home;
7pub(crate) mod pin;
8pub(crate) mod privacy;
9pub(crate) mod server;
10pub mod sharing;
11pub(crate) mod webhook;
12
13use self::{
14    account::MyPlexAccount, announcements::AnnouncementsManager, claim_token::ClaimToken,
15    device::DeviceManager, discover::Discover, home::HomeManager, pin::PinManager,
16    privacy::Privacy, sharing::Sharing, webhook::WebhookManager,
17};
18use crate::{
19    http_client::{HttpClient, HttpClientBuilder, Request},
20    media_container::server::Feature,
21    url::{MYPLEX_SERVERS, MYPLEX_SIGNIN_PATH, MYPLEX_SIGNOUT_PATH, MYPLEX_USER_INFO_PATH},
22    Error, Result,
23};
24use http::StatusCode;
25use isahc::AsyncBody;
26use secrecy::{ExposeSecret, SecretString};
27
28#[derive(Debug, Clone)]
29pub struct MyPlex {
30    client: HttpClient,
31    account: Option<MyPlexAccount>,
32}
33
34impl MyPlex {
35    pub fn new(client: HttpClient) -> Self {
36        Self {
37            client,
38            account: None,
39        }
40    }
41
42    async fn login_internal(
43        username: &str,
44        password: &str,
45        client: HttpClient,
46        extra_params: &[(&str, &str)],
47    ) -> Result<Self> {
48        if client.is_authenticated() {
49            return Err(Error::ClientAuthenticated);
50        }
51
52        let mut params = vec![
53            ("login", username),
54            ("password", password),
55            ("rememberMe", "true"),
56        ];
57        for (key, value) in extra_params {
58            params.push((key, value));
59        }
60
61        Self::build_from_signin_response(&client, client.post(MYPLEX_SIGNIN_PATH).form(&params)?)
62            .await
63    }
64
65    #[tracing::instrument(level = "debug", skip(password, client))]
66    async fn login(username: &str, password: &str, client: HttpClient) -> Result<Self> {
67        Self::login_internal(username, password, client, &[]).await
68    }
69
70    #[tracing::instrument(
71        name = "MyPlex::login_with_otp",
72        level = "debug",
73        skip(password, client)
74    )]
75    async fn login_with_otp(
76        username: &str,
77        password: &str,
78        verification_code: &str,
79        client: HttpClient,
80    ) -> Result<Self> {
81        Self::login_internal(
82            username,
83            password,
84            client,
85            &[("verificationCode", verification_code)],
86        )
87        .await
88    }
89
90    #[tracing::instrument(level = "debug", skip(self))]
91    pub async fn refresh(self) -> Result<Self> {
92        if !self.client.is_authenticated() {
93            return Err(Error::ClientNotAuthenticated);
94        }
95
96        Self::build_from_signin_response(
97            &self.client,
98            self.client.get(MYPLEX_USER_INFO_PATH).body(())?,
99        )
100        .await
101    }
102
103    async fn build_from_signin_response<B>(
104        client: &HttpClient,
105        request: Request<'_, B>,
106    ) -> Result<Self>
107    where
108        B: Into<AsyncBody>,
109    {
110        let account: account::MyPlexAccount = request.json().await?;
111        Ok(Self {
112            client: client.clone().set_x_plex_token(account.auth_token.clone()),
113            account: Some(account),
114        })
115    }
116
117    pub fn client(&self) -> &HttpClient {
118        &self.client
119    }
120
121    /// Get a claim token from the API, which can be used for attaching a server to your account.
122    /// See <https://hub.docker.com/r/plexinc/pms-docker> for details, look for "PLEX_CLAIM".
123    pub async fn claim_token(&self) -> Result<ClaimToken> {
124        if !self.client.is_authenticated() {
125            return Err(Error::ClientNotAuthenticated);
126        }
127
128        ClaimToken::new(&self.client).await
129    }
130
131    /// Get privacy settings for your account. You can update the settings using the returned object.
132    /// See [Privacy Preferences on plex.tv](https://www.plex.tv/about/privacy-legal/privacy-preferences/#opd) for details.
133    pub async fn privacy(&self) -> Result<Privacy> {
134        if !self.client.is_authenticated() {
135            return Err(Error::ClientNotAuthenticated);
136        }
137
138        Privacy::new(self.client.clone()).await
139    }
140
141    pub fn sharing(&self) -> Result<Sharing> {
142        if !self.client.is_authenticated() {
143            return Err(Error::ClientNotAuthenticated);
144        }
145
146        Ok(Sharing::new(self))
147    }
148
149    #[tracing::instrument(level = "debug", skip(self))]
150    pub async fn server_info(&self, machine_identifier: &str) -> Result<server::ServerInfo> {
151        if !self.client.is_authenticated() {
152            return Err(Error::ClientNotAuthenticated);
153        }
154
155        self.client
156            .get(format!("{}/{}", MYPLEX_SERVERS, machine_identifier))
157            .json()
158            .await
159    }
160
161    pub fn available_features(&self) -> Option<&Vec<Feature>> {
162        return self
163            .account
164            .as_ref()
165            .map(|account| &account.subscription.features);
166    }
167
168    pub fn account(&self) -> Option<&MyPlexAccount> {
169        return self.account.as_ref();
170    }
171
172    #[tracing::instrument(level = "debug", skip(self))]
173    pub async fn webhook_manager(&self) -> Result<WebhookManager> {
174        if !self.client.is_authenticated() {
175            return Err(Error::ClientNotAuthenticated);
176        }
177
178        if let Some(features) = self.available_features() {
179            if !features.contains(&Feature::Webhooks) {
180                return Err(Error::SubscriptionFeatureNotAvailable(Feature::Webhooks));
181            }
182        }
183
184        WebhookManager::new(self.client.clone()).await
185    }
186
187    pub fn device_manager(&self) -> Result<DeviceManager> {
188        if !self.client.is_authenticated() {
189            return Err(Error::ClientNotAuthenticated);
190        }
191
192        Ok(DeviceManager::new(self.client.clone()))
193    }
194
195    pub fn pin_manager(&self) -> Result<PinManager> {
196        if !self.client.is_authenticated() {
197            return Err(Error::ClientNotAuthenticated);
198        }
199
200        Ok(PinManager::new(self.client.clone()))
201    }
202
203    pub async fn announcements(&self) -> Result<AnnouncementsManager> {
204        AnnouncementsManager::new(self.client.clone()).await
205    }
206
207    /// Sign out of your account. It's highly recommended to call this method when you're done using the API.
208    /// At least when you obtained the MyPlex instance using [MyPlex::login](struct.MyPlex.html#method.login).
209    #[tracing::instrument(level = "debug", skip(self))]
210    pub async fn signout(self) -> Result {
211        if !self.client.is_authenticated() {
212            return Err(Error::ClientNotAuthenticated);
213        }
214
215        let response = self
216            .client
217            .delete(MYPLEX_SIGNOUT_PATH)
218            .body(())?
219            .send()
220            .await?;
221
222        match response.status() {
223            StatusCode::NO_CONTENT => Ok(()),
224            _ => Err(Error::from_response(response).await),
225        }
226    }
227
228    /// Various controls over Plex Home.
229    pub fn home(&self) -> Result<HomeManager> {
230        if !self.client.is_authenticated() {
231            return Err(Error::ClientNotAuthenticated);
232        }
233
234        Ok(HomeManager {
235            client: self.client.clone(),
236        })
237    }
238
239    /// Interface for discovering new movies & shows (includes watchlist)
240    pub async fn discover(&self) -> Result<Discover> {
241        if !self.client.is_authenticated() {
242            return Err(Error::ClientNotAuthenticated);
243        }
244
245        Discover::new(&self.client).await
246    }
247}
248
249#[derive(Debug, Clone)]
250pub struct MyPlexBuilder<'a> {
251    client: Option<HttpClient>,
252    token: Option<SecretString>,
253    username: Option<&'a str>,
254    password: Option<SecretString>,
255    otp: Option<SecretString>,
256    test_token_auth: bool,
257}
258
259impl<'a> Default for MyPlexBuilder<'a> {
260    fn default() -> Self {
261        Self {
262            client: None,
263            token: None,
264            username: None,
265            password: None,
266            otp: None,
267            test_token_auth: true,
268        }
269    }
270}
271
272impl MyPlexBuilder<'_> {
273    pub fn set_client(self, client: HttpClient) -> Self {
274        Self {
275            client: Some(client),
276            token: self.token,
277            username: self.username,
278            password: self.password,
279            otp: self.otp,
280            test_token_auth: self.test_token_auth,
281        }
282    }
283
284    pub fn set_test_token_auth(self, test_token_auth: bool) -> Self {
285        Self {
286            client: self.client,
287            token: self.token,
288            username: self.username,
289            password: self.password,
290            otp: self.otp,
291            test_token_auth,
292        }
293    }
294
295    pub async fn build(self) -> Result<MyPlex> {
296        let mut client = if let Some(client) = self.client {
297            client
298        } else {
299            HttpClientBuilder::default().build()?
300        };
301
302        if let (Some(username), Some(password)) = (self.username, self.password) {
303            if let Some(otp) = self.otp {
304                return MyPlex::login_with_otp(
305                    username,
306                    password.expose_secret(),
307                    otp.expose_secret(),
308                    client,
309                )
310                .await;
311            } else {
312                return MyPlex::login(username, password.expose_secret(), client).await;
313            }
314        }
315
316        if self.otp.is_some() {
317            return Err(Error::UselessOtp);
318        }
319
320        if let Some(token) = self.token {
321            client = client.set_x_plex_token(token);
322        }
323
324        let mut plex = MyPlex::new(client);
325
326        if self.test_token_auth {
327            plex = plex.refresh().await?;
328        }
329
330        Ok(plex)
331    }
332}
333
334impl<'a> MyPlexBuilder<'a> {
335    pub fn set_token<T>(self, token: T) -> Self
336    where
337        T: Into<SecretString>,
338    {
339        Self {
340            client: self.client,
341            token: Some(token.into()),
342            username: self.username,
343            password: self.password,
344            otp: self.otp,
345            test_token_auth: self.test_token_auth,
346        }
347    }
348
349    pub fn set_username_and_password<T>(self, username: &'a str, password: T) -> Self
350    where
351        T: Into<SecretString>,
352    {
353        Self {
354            client: self.client,
355            token: self.token,
356            username: Some(username),
357            password: Some(password.into()),
358            otp: self.otp,
359            test_token_auth: self.test_token_auth,
360        }
361    }
362
363    pub fn set_otp<T>(self, otp: T) -> Self
364    where
365        T: Into<SecretString>,
366    {
367        Self {
368            client: self.client,
369            token: self.token,
370            username: self.username,
371            password: self.password,
372            otp: Some(otp.into()),
373            test_token_auth: self.test_token_auth,
374        }
375    }
376}