plex_api/myplex/
pin.rs

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