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