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(¶ms)?)
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 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 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 #[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 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 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}