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