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    isahc_compat::StatusCodeExt,
21    media_container::server::Feature,
22    url::{MYPLEX_SERVERS, MYPLEX_SIGNIN_PATH, MYPLEX_SIGNOUT_PATH, MYPLEX_USER_INFO_PATH},
23    Error, Result,
24};
25use http::StatusCode;
26use isahc::AsyncBody;
27use secrecy::{ExposeSecret, SecretString};
28
29#[derive(Debug, Clone)]
30pub struct MyPlex {
31    client: HttpClient,
32    account: Option<MyPlexAccount>,
33}
34
35impl MyPlex {
36    pub fn new(client: HttpClient) -> Self {
37        Self {
38            client,
39            account: None,
40        }
41    }
42
43    async fn login_internal(
44        username: &str,
45        password: &str,
46        client: HttpClient,
47        extra_params: &[(&str, &str)],
48    ) -> Result<Self> {
49        if client.is_authenticated() {
50            return Err(Error::ClientAuthenticated);
51        }
52
53        let mut params = vec![
54            ("login", username),
55            ("password", password),
56            ("rememberMe", "true"),
57        ];
58        for (key, value) in extra_params {
59            params.push((key, value));
60        }
61
62        Self::build_from_signin_response(&client, client.post(MYPLEX_SIGNIN_PATH).form(&params)?)
63            .await
64    }
65
66    #[tracing::instrument(level = "debug", skip(password, client))]
67    async fn login(username: &str, password: &str, client: HttpClient) -> Result<Self> {
68        Self::login_internal(username, password, client, &[]).await
69    }
70
71    #[tracing::instrument(
72        name = "MyPlex::login_with_otp",
73        level = "debug",
74        skip(password, client)
75    )]
76    async fn login_with_otp(
77        username: &str,
78        password: &str,
79        verification_code: &str,
80        client: HttpClient,
81    ) -> Result<Self> {
82        Self::login_internal(
83            username,
84            password,
85            client,
86            &[("verificationCode", verification_code)],
87        )
88        .await
89    }
90
91    #[tracing::instrument(level = "debug", skip(self))]
92    pub async fn refresh(self) -> Result<Self> {
93        if !self.client.is_authenticated() {
94            return Err(Error::ClientNotAuthenticated);
95        }
96
97        Self::build_from_signin_response(
98            &self.client,
99            self.client.get(MYPLEX_USER_INFO_PATH).body(())?,
100        )
101        .await
102    }
103
104    async fn build_from_signin_response<B>(
105        client: &HttpClient,
106        request: Request<'_, B>,
107    ) -> Result<Self>
108    where
109        B: Into<AsyncBody>,
110    {
111        let account: account::MyPlexAccount = request.json().await?;
112        Ok(Self {
113            client: client.clone().set_x_plex_token(account.auth_token.clone()),
114            account: Some(account),
115        })
116    }
117
118    pub fn client(&self) -> &HttpClient {
119        &self.client
120    }
121
122    /// Get a claim token from the API, which can be used for attaching a server to your account.
123    /// See <https://hub.docker.com/r/plexinc/pms-docker> for details, look for "PLEX_CLAIM".
124    pub async fn claim_token(&self) -> Result<ClaimToken> {
125        if !self.client.is_authenticated() {
126            return Err(Error::ClientNotAuthenticated);
127        }
128
129        ClaimToken::new(&self.client).await
130    }
131
132    /// Get privacy settings for your account. You can update the settings using the returned object.
133    /// See [Privacy Preferences on plex.tv](https://www.plex.tv/about/privacy-legal/privacy-preferences/#opd) for details.
134    pub async fn privacy(&self) -> Result<Privacy> {
135        if !self.client.is_authenticated() {
136            return Err(Error::ClientNotAuthenticated);
137        }
138
139        Privacy::new(self.client.clone()).await
140    }
141
142    pub fn sharing(&'_ self) -> Result<Sharing<'_>> {
143        if !self.client.is_authenticated() {
144            return Err(Error::ClientNotAuthenticated);
145        }
146
147        Ok(Sharing::new(self))
148    }
149
150    #[tracing::instrument(level = "debug", skip(self))]
151    pub async fn server_info(&self, machine_identifier: &str) -> Result<server::ServerInfo> {
152        if !self.client.is_authenticated() {
153            return Err(Error::ClientNotAuthenticated);
154        }
155
156        self.client
157            .get(format!("{}/{}", MYPLEX_SERVERS, machine_identifier))
158            .json()
159            .await
160    }
161
162    pub fn available_features(&self) -> Option<&Vec<Feature>> {
163        self.account
164            .as_ref()
165            .map(|account| &account.subscription.features)
166    }
167
168    pub fn account(&self) -> Option<&MyPlexAccount> {
169        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().as_http_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}