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 #[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 #[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 #[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 #[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 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 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 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 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}