1use crate::{
2 isahc_compat::StatusCodeExt,
3 url::{MYPLEX_PINS, MYPLEX_PINS_LINK},
4 Error, HttpClient, Result,
5};
6use http::StatusCode;
7use isahc::AsyncReadResponseExt;
8use serde::Deserialize;
9use time::OffsetDateTime;
10
11pub struct PinManager {
12 client: HttpClient,
13}
14
15impl PinManager {
16 pub fn new(client: HttpClient) -> Self {
17 Self { client }
18 }
19
20 #[tracing::instrument(level = "debug", skip(self))]
21 pub async fn link(&self, code: &str) -> Result {
22 if !self.client.is_authenticated() {
23 return Err(Error::ClientNotAuthenticated);
24 }
25
26 let response = self
27 .client
28 .putm(MYPLEX_PINS_LINK)
29 .header("X-Plex-Product", "Plex SSO")
30 .form(&[("code", code)])?
31 .send()
32 .await?;
33
34 if response.status().as_http_status() == StatusCode::NO_CONTENT {
35 Ok(())
36 } else {
37 Err(Error::from_response(response).await)
38 }
39 }
40
41 #[tracing::instrument(level = "debug", skip(self))]
42 pub async fn pin(&self) -> Result<Pin<'_>> {
43 if self.client.is_authenticated() {
44 return Err(Error::ClientAuthenticated);
45 }
46
47 let mut response = self
48 .client
49 .post(MYPLEX_PINS)
50 .header("Accept", "application/json")
51 .send()
52 .await?;
53
54 if response.status().as_http_status() == StatusCode::CREATED {
55 let pin = response.json::<PinInfo>().await?;
56 Ok(Pin {
57 client: &self.client,
58 pin,
59 })
60 } else {
61 Err(Error::from_response(response).await)
62 }
63 }
64}
65
66#[derive(Debug)]
67pub struct Pin<'a> {
68 client: &'a HttpClient,
69 pub pin: PinInfo,
70}
71
72impl<'a> Pin<'a> {
73 pub fn code(&self) -> &str {
75 &self.pin.code
76 }
77
78 pub fn is_expired(&self) -> bool {
80 self.pin.expires_at < OffsetDateTime::now_utc()
81 }
82
83 #[tracing::instrument(level = "debug", skip(self), fields(self.pin.id = self.pin.id))]
85 pub async fn check(&self) -> Result<PinInfo> {
86 if self.is_expired() {
87 return Err(Error::PinExpired);
88 }
89
90 let url = format!("{}/{}", MYPLEX_PINS, self.pin.id);
91 let pin: PinInfo = self.client.get(url).json().await?;
92
93 if pin.auth_token.is_some() {
94 Ok(pin)
95 } else {
96 Err(Error::PinNotLinked)
97 }
98 }
99}
100
101#[derive(Deserialize, Debug)]
102#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
103#[serde(rename_all = "camelCase")]
104pub struct PinInfo {
105 pub id: u32,
106 pub code: String,
107 pub product: String,
108 pub trusted: bool,
109 pub client_identifier: String,
110 pub location: Location,
111 pub expires_in: u32,
112 #[serde(with = "time::serde::rfc3339")]
113 pub created_at: OffsetDateTime,
114 #[serde(with = "time::serde::rfc3339")]
115 pub expires_at: OffsetDateTime,
116 pub auth_token: Option<String>,
117 pub new_registration: Option<bool>,
118}
119
120#[derive(Deserialize, Debug)]
121#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
122pub struct Location {
123 pub code: String,
124 pub european_union_member: bool,
125 pub continent_code: String,
126 pub country: String,
127 pub city: String,
128 pub time_zone: String,
129 pub postal_code: String,
130 pub in_privacy_restricted_country: bool,
131 pub subdivisions: String,
132 pub coordinates: String,
133}