plex_api/myplex/
pin.rs

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    /// Returns the code that should be displayed to a user.
73    pub fn code(&self) -> &str {
74        &self.pin.code
75    }
76
77    /// Checks if the pin is still valid.
78    pub fn is_expired(&self) -> bool {
79        self.pin.expires_at < OffsetDateTime::now_utc()
80    }
81
82    /// Check if the pin was linked by a user.
83    #[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}