plex_api/server/
mod.rs

1pub mod library;
2pub(crate) mod prefs;
3pub mod transcode;
4
5use self::{
6    library::{metadata_items, FromMetadata, Item, Library, MediaItem, MetadataItem},
7    prefs::Preferences,
8    transcode::{
9        transcode_artwork, transcode_session_stats, ArtTranscodeOptions, TranscodeSession,
10        TranscodeSessionsMediaContainer,
11    },
12};
13#[cfg(not(feature = "tests_deny_unknown_fields"))]
14use crate::media_container::server::library::LibraryType;
15use crate::{
16    http_client::HttpClient,
17    media_container::{
18        server::{library::ContentDirectory, MediaProviderFeature, Server as ServerMediaContainer},
19        MediaContainerWrapper,
20    },
21    myplex::MyPlex,
22    url::{
23        SERVER_MEDIA_PROVIDERS, SERVER_MYPLEX_ACCOUNT, SERVER_MYPLEX_CLAIM, SERVER_SCROBBLE,
24        SERVER_TIMELINE, SERVER_TRANSCODE_SESSIONS, SERVER_UNSCROBBLE,
25    },
26    Error, HttpClientBuilder, Result,
27};
28use core::convert::TryFrom;
29use futures::AsyncWrite;
30use http::{StatusCode, Uri};
31use isahc::AsyncReadResponseExt;
32use std::{
33    collections::HashMap,
34    fmt::{self, Debug},
35};
36
37struct Query {
38    params: HashMap<String, String>,
39}
40
41impl Query {
42    fn new() -> Self {
43        Self {
44            params: HashMap::new(),
45        }
46    }
47
48    fn param<N: Into<String>, V: Into<String>>(mut self, name: N, value: V) -> Self {
49        self.params.insert(name.into(), value.into());
50        self
51    }
52}
53
54impl fmt::Display for Query {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.pad(&serde_urlencoded::to_string(&self.params).unwrap())
57    }
58}
59
60#[derive(Debug, Clone)]
61pub struct Server {
62    client: HttpClient,
63    pub myplex_api_url: Uri,
64    pub media_container: ServerMediaContainer,
65}
66
67impl Server {
68    async fn build(client: HttpClient, myplex_api_url: Uri) -> Result<Self> {
69        let media_container_wrapper: MediaContainerWrapper<ServerMediaContainer> =
70            client.get(SERVER_MEDIA_PROVIDERS).json().await?;
71
72        Ok(Self {
73            media_container: media_container_wrapper.media_container,
74            client,
75            myplex_api_url,
76        })
77    }
78
79    #[tracing::instrument(level = "debug", skip(client))]
80    pub async fn new<U>(url: U, client: HttpClient) -> Result<Self>
81    where
82        U: Debug,
83        Uri: TryFrom<U>,
84        <Uri as TryFrom<U>>::Error: Into<http::Error>,
85    {
86        let myplex_api_url = client.api_url.clone();
87        Self::build(
88            HttpClientBuilder::from(client).set_api_url(url).build()?,
89            myplex_api_url,
90        )
91        .await
92    }
93
94    fn content(&self) -> Option<&Vec<ContentDirectory>> {
95        if let Some(provider) = self
96            .media_container
97            .media_providers
98            .iter()
99            .find(|p| p.identifier == "com.plexapp.plugins.library")
100        {
101            for feature in &provider.features {
102                if let MediaProviderFeature::Content {
103                    key: _,
104                    ref directory,
105                } = feature
106                {
107                    return Some(directory);
108                }
109            }
110        }
111
112        None
113    }
114
115    pub fn libraries(&self) -> Vec<Library> {
116        if let Some(content) = self.content() {
117            content
118                .iter()
119                .filter_map(|d| match d {
120                    ContentDirectory::Media(lib) => match lib.library_type {
121                        #[cfg(not(feature = "tests_deny_unknown_fields"))]
122                        LibraryType::Unknown => None,
123                        _ => Some(Library::new(self.client.clone(), *lib.clone())),
124                    },
125                    _ => None,
126                })
127                .collect()
128        } else {
129            Vec::new()
130        }
131    }
132
133    /// Given the path to some item's artwork (`art` or `thumb` properties for
134    /// example but many other types of images will work) this will request a
135    /// scaled version of that image be written to the passed writer as a JPEG.
136    /// The image will always maintain its aspect ratio.
137    #[tracing::instrument(
138        name = "Server::transcode_artwork",
139        level = "debug",
140        skip(self, writer)
141    )]
142    pub async fn transcode_artwork<W>(
143        &self,
144        art: &str,
145        width: u32,
146        height: u32,
147        options: ArtTranscodeOptions,
148        writer: W,
149    ) -> Result<()>
150    where
151        W: AsyncWrite + Unpin,
152    {
153        transcode_artwork(&self.client, art, width, height, options, writer).await
154    }
155
156    /// Retrieves a list of the current transcode sessions.
157    #[tracing::instrument(level = "debug", skip(self))]
158    pub async fn transcode_sessions(&self) -> Result<Vec<TranscodeSession>> {
159        let wrapper: MediaContainerWrapper<TranscodeSessionsMediaContainer> =
160            self.client.get(SERVER_TRANSCODE_SESSIONS).json().await?;
161
162        Ok(wrapper
163            .media_container
164            .transcode_sessions
165            .into_iter()
166            .map(move |stats| TranscodeSession::from_stats(self.client.clone(), stats))
167            .collect())
168    }
169
170    /// Retrieves the transcode session with the passed ID.
171    #[tracing::instrument(level = "debug", skip(self))]
172    pub async fn transcode_session(&self, session_id: &str) -> Result<TranscodeSession> {
173        let stats = transcode_session_stats(&self.client, session_id).await?;
174        Ok(TranscodeSession::from_stats(self.client.clone(), stats))
175    }
176
177    /// Allows retrieving media, playlists, collections and other items using
178    /// their rating key.
179    #[tracing::instrument(level = "debug", skip(self))]
180    pub async fn item_by_id(&self, rating_key: &str) -> Result<Item> {
181        let path = format!("/library/metadata/{rating_key}?includeConcerts=1&includeExtras=1&includePopularLeaves=1&includePreferences=1&includeReviews=1&includeOnDeck=1&includeChapters=1&includeStations=1&includeExternalMedia=1&asyncAugmentMetadata=1&asyncCheckFiles=1&asyncRefreshAnalysis=1&asyncRefreshLocalMediaAgent=1&includeMarkers=1");
182
183        match metadata_items(&self.client, &path).await {
184            Ok(items) => items.into_iter().next().ok_or(Error::ItemNotFound),
185            Err(Error::UnexpectedApiResponse {
186                status_code,
187                content,
188            }) => {
189                // A 404 error indicates the item does not exist.
190                if status_code == 404 {
191                    Err(Error::ItemNotFound)
192                } else {
193                    Err(Error::UnexpectedApiResponse {
194                        status_code,
195                        content,
196                    })
197                }
198            }
199            Err(err) => Err(err),
200        }
201    }
202
203    /// Marks a media item as fully watched increasing its view count by one.
204    pub async fn mark_watched<M: MediaItem + FromMetadata>(&self, item: &M) -> Result<M> {
205        let rating_key = item.rating_key();
206        let path =
207            format!("{SERVER_SCROBBLE}?identifier=com.plexapp.plugins.library&key={rating_key}");
208
209        self.client.get(path).consume().await?;
210
211        let item = self.item_by_id(rating_key).await?;
212        Ok(M::from_metadata(
213            self.client.clone(),
214            item.metadata().clone(),
215        ))
216    }
217
218    /// Marks a media item as unwatched.
219    pub async fn mark_unwatched<M: MediaItem + FromMetadata>(&self, item: &M) -> Result<M> {
220        let rating_key = item.rating_key();
221        let path =
222            format!("{SERVER_UNSCROBBLE}?identifier=com.plexapp.plugins.library&key={rating_key}");
223
224        self.client.get(path).consume().await?;
225
226        let item = self.item_by_id(rating_key).await?;
227        Ok(M::from_metadata(
228            self.client.clone(),
229            item.metadata().clone(),
230        ))
231    }
232
233    /// Sets a media item's playback position in milliseconds. The server currently ignores any
234    /// positions equal to or less than 60000ms. The time sets the time the item was last viewed.
235    pub async fn update_timeline<M: MediaItem + FromMetadata>(
236        &self,
237        item: &M,
238        position: u64,
239    ) -> Result<M> {
240        let rating_key = item.rating_key();
241        let query = Query::new()
242            .param("key", format!("/library/metadata/{rating_key}"))
243            .param("ratingKey", rating_key)
244            .param("offline", "1")
245            .param("state", "playing")
246            .param("time", position.to_string());
247
248        self.client
249            .get(format!("{SERVER_TIMELINE}?{query}"))
250            .consume()
251            .await?;
252
253        let item = self.item_by_id(rating_key).await?;
254        Ok(M::from_metadata(
255            self.client.clone(),
256            item.metadata().clone(),
257        ))
258    }
259
260    #[tracing::instrument(level = "debug", skip(self))]
261    pub async fn refresh(self) -> Result<Self> {
262        Self::build(self.client, self.myplex_api_url).await
263    }
264
265    pub fn myplex(&self) -> Result<MyPlex> {
266        self.myplex_with_api_url(self.myplex_api_url.clone())
267    }
268
269    #[tracing::instrument(level = "debug", skip(self))]
270    pub async fn claim(self, claim_token: &str) -> Result<Self> {
271        let url = format!(
272            "{}?{}",
273            SERVER_MYPLEX_CLAIM,
274            serde_urlencoded::to_string([("token", claim_token)])?
275        );
276        let mut response = self.client.post(url).send().await?;
277
278        if response.status() == StatusCode::OK {
279            response.consume().await?;
280            self.refresh().await
281        } else {
282            Err(crate::Error::from_response(response).await)
283        }
284    }
285
286    #[tracing::instrument(level = "debug", skip(self))]
287    pub async fn unclaim(self) -> Result<Self> {
288        let mut response = self.client.delete(SERVER_MYPLEX_ACCOUNT).send().await?;
289
290        if response.status() == StatusCode::OK {
291            response.consume().await?;
292            self.refresh().await
293        } else {
294            Err(crate::Error::from_response(response).await)
295        }
296    }
297
298    pub fn myplex_with_api_url<U>(&self, api_url: U) -> Result<MyPlex>
299    where
300        Uri: TryFrom<U>,
301        <Uri as TryFrom<U>>::Error: Into<http::Error>,
302    {
303        Ok(MyPlex::new(
304            HttpClientBuilder::from(self.client.clone())
305                .set_api_url(api_url)
306                .build()?,
307        ))
308    }
309
310    pub fn client(&self) -> &HttpClient {
311        &self.client
312    }
313
314    pub async fn preferences<'a>(&self) -> Result<Preferences<'a>> {
315        Preferences::new(&self.client).await
316    }
317
318    pub fn machine_identifier(&self) -> &str {
319        &self.media_container.machine_identifier
320    }
321}