plex_api/
error.rs

1use crate::media_container::server::Feature;
2use isahc::{AsyncBody, AsyncReadResponseExt, Response as HttpResponse};
3use serde::Deserialize;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum Error {
8    #[error("Authenticated client must be provided.")]
9    ClientNotAuthenticated,
10    #[error("Non-authenticated client must be provided.")]
11    ClientAuthenticated,
12    #[error("Unable to deserialize JSON: {source}.")]
13    JsonDeserealiseError {
14        #[from]
15        source: serde_json::Error,
16    },
17    #[error("Unable to deserialize XML: {source}.")]
18    XmlDeserealiseError {
19        #[from]
20        source: quick_xml::de::DeError,
21    },
22    #[error("{source}")]
23    UrlencodingError {
24        #[from]
25        source: serde_urlencoded::ser::Error,
26    },
27    #[error("{source}")]
28    HttpError {
29        #[from]
30        source: http::Error,
31    },
32    #[error("{source}")]
33    IsahcHttpError {
34        #[from]
35        source: isahc::http::Error,
36    },
37    #[error("{source}")]
38    IsahcError {
39        #[from]
40        source: isahc::Error,
41    },
42    #[error("{source}")]
43    StdIoError {
44        #[from]
45        source: std::io::Error,
46    },
47    #[error("Error while communicating with MyPlexApi: {errors:?}.")]
48    MyPlexErrorResponse { errors: Vec<Self> },
49    #[error("Error occurred while communicating to MyPlex API: #{code} - {message}.")]
50    MyPlexApiError { code: i32, message: String },
51    #[error("Failed to get claim token: {0}.")]
52    FailedToGetClaimToken(String),
53    #[error("Unexpected API response: HTTP {status_code}, content: {content}.")]
54    UnexpectedApiResponse { status_code: u16, content: String },
55    #[error("The requested webhook wasn't found: {0}.")]
56    WebhookNotFound(String),
57    #[error("The mandatory feature is not available: {0}.")]
58    SubscriptionFeatureNotAvailable(Feature),
59    #[error("OTP is required for the authentication.")]
60    OtpRequired,
61    #[error("OTP is provided, but no username/password.")]
62    UselessOtp,
63    #[error("Connecting to the device is not supported.")]
64    DeviceConnectionNotSupported,
65    #[error("Device doesn't have any exposed connection endpoints.")]
66    DeviceConnectionsIsEmpty,
67    #[error("Requested unknown setting: {0}.")]
68    RequestedSettingNotFound(String),
69    #[error("You can't set setting to a value of a different type.")]
70    IncompatibleSettingValues,
71    #[error("Provided pin is already expired.")]
72    PinExpired,
73    #[error("Provided pin is not linked yet.")]
74    PinNotLinked,
75    #[error("Item requested was not found on the server.")]
76    ItemNotFound,
77    #[error("The requested transcode parameters were invalid.")]
78    InvalidTranscodeSettings,
79    #[error("The transcode request failed: {0}.")]
80    TranscodeError(String),
81    #[error("The server thinks the client should just play the original media.")]
82    TranscodeRefused,
83    #[error("Transcoding is incomplete.")]
84    TranscodeIncomplete,
85    #[error("Invalid header value.")]
86    InvalidHeaderValue,
87    #[error("Unknown container format.")]
88    UnknownContainerFormat(String),
89    #[error("Only invites with status pending_received can be accepted.")]
90    InviteAcceptingNotPendingReceived,
91    #[error("Unexpected error. Please create a bug report.")]
92    UnexpectedError,
93}
94
95const PLEX_API_ERROR_CODE_AUTH_OTP_REQUIRED: i32 = 1029;
96
97impl Error {
98    pub async fn from_response(mut response: HttpResponse<AsyncBody>) -> Self {
99        let status_code = response.status().as_u16();
100        let response_body = match response.text().await {
101            Ok(body) => body,
102            Err(err) => {
103                return err.into();
104            }
105        };
106
107        let err: Result<MyPlexApiErrorResponse, Error>;
108        if let Some(content_type) = response.headers().get("Content-type") {
109            match content_type.to_str().unwrap().split("; ").next().unwrap() {
110                "application/xml" => {
111                    err = quick_xml::de::from_str::<MyPlexApiErrorResponse>(&response_body)
112                        .map_err(|e| e.into())
113                }
114                _ => {
115                    err = serde_json::from_str::<MyPlexApiErrorResponse>(&response_body)
116                        .map_err(|e| e.into())
117                }
118            }
119        } else {
120            err = serde_json::from_str::<MyPlexApiErrorResponse>(&response_body)
121                .map_err(|e| e.into());
122        }
123
124        match err {
125            Ok(r) => {
126                if r.errors.len() == 1 && r.errors[0].code == PLEX_API_ERROR_CODE_AUTH_OTP_REQUIRED
127                {
128                    Self::OtpRequired
129                } else {
130                    r.into()
131                }
132            }
133            Err(_) => Self::UnexpectedApiResponse {
134                status_code,
135                content: response_body,
136            },
137        }
138    }
139}
140
141#[derive(Deserialize, Debug, Clone)]
142#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
143struct MyPlexApiError {
144    code: i32,
145    message: String,
146
147    /// HTTP status code for the error. Not used, keeping it here to reflect the complete API response.
148    #[allow(dead_code)]
149    status: u16,
150}
151
152#[derive(Deserialize, Debug, Clone)]
153#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
154pub(crate) struct MyPlexApiErrorResponse {
155    errors: Vec<MyPlexApiError>,
156}
157
158impl From<MyPlexApiError> for Error {
159    fn from(error: MyPlexApiError) -> Self {
160        Self::MyPlexApiError {
161            code: error.code,
162            message: error.message,
163        }
164    }
165}
166
167impl From<MyPlexApiErrorResponse> for Error {
168    fn from(r: MyPlexApiErrorResponse) -> Self {
169        Self::MyPlexErrorResponse {
170            errors: r.errors.into_iter().map(|e| e.into()).collect(),
171        }
172    }
173}