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 session::{transcode_session_stats, TranscodeSession, TranscodeSessionsMediaContainer},
10 transcode_artwork, ArtTranscodeOptions,
11 },
12};
13#[cfg(not(feature = "tests_deny_unknown_fields"))]
14use crate::media_container::server::library::LibraryType;
15use crate::{
16 http_client::HttpClient,
17 isahc_compat::StatusCodeExt,
18 media_container::{
19 server::{library::ContentDirectory, MediaProviderFeature, Server as ServerMediaContainer},
20 MediaContainerWrapper,
21 },
22 myplex::MyPlex,
23 transcode::download_queue::DownloadQueue,
24 url::{
25 SERVER_MEDIA_PROVIDERS, SERVER_MYPLEX_ACCOUNT, SERVER_MYPLEX_CLAIM, SERVER_SCROBBLE,
26 SERVER_TIMELINE, SERVER_TRANSCODE_SESSIONS, SERVER_UNSCROBBLE,
27 },
28 Error, HttpClientBuilder, Result,
29};
30use futures::AsyncWrite;
31use http::{StatusCode, Uri};
32use isahc::AsyncReadResponseExt;
33use std::{
34 collections::HashMap,
35 fmt::{self, Debug},
36};
37
38struct Query {
39 params: HashMap<String, String>,
40}
41
42impl Query {
43 fn new() -> Self {
44 Self {
45 params: HashMap::new(),
46 }
47 }
48
49 fn append(mut self, other: HashMap<String, String>) -> Self {
50 self.params.extend(other);
51 self
52 }
53
54 fn param<N: Into<String>, V: Into<String>>(mut self, name: N, value: V) -> Self {
55 self.params.insert(name.into(), value.into());
56 self
57 }
58}
59
60impl From<Query> for HashMap<String, String> {
61 fn from(query: Query) -> Self {
62 query.params
63 }
64}
65
66impl fmt::Display for Query {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.pad(&serde_urlencoded::to_string(&self.params).unwrap())
69 }
70}
71
72#[derive(Debug, Clone)]
73pub struct Server {
74 client: HttpClient,
75 pub myplex_api_url: Uri,
76 pub media_container: ServerMediaContainer,
77}
78
79impl Server {
80 async fn build(client: HttpClient, myplex_api_url: Uri) -> Result<Self> {
81 let media_container_wrapper: MediaContainerWrapper<ServerMediaContainer> =
82 client.get(SERVER_MEDIA_PROVIDERS).json().await?;
83
84 Ok(Self {
85 media_container: media_container_wrapper.media_container,
86 client,
87 myplex_api_url,
88 })
89 }
90
91 #[tracing::instrument(level = "debug", skip(client))]
92 pub async fn new<U>(url: U, client: HttpClient) -> Result<Self>
93 where
94 U: Debug,
95 Uri: TryFrom<U>,
96 <Uri as TryFrom<U>>::Error: Into<http::Error>,
97 {
98 let myplex_api_url = client.api_url.clone();
99 Self::build(
100 HttpClientBuilder::from(client).set_api_url(url).build()?,
101 myplex_api_url,
102 )
103 .await
104 }
105
106 fn content(&self) -> Option<&Vec<ContentDirectory>> {
107 if let Some(provider) = self
108 .media_container
109 .media_providers
110 .iter()
111 .find(|p| p.identifier == "com.plexapp.plugins.library")
112 {
113 for feature in &provider.features {
114 if let MediaProviderFeature::Content {
115 key: _,
116 ref directory,
117 } = feature
118 {
119 return Some(directory);
120 }
121 }
122 }
123
124 None
125 }
126
127 pub fn libraries(&self) -> Vec<Library> {
128 if let Some(content) = self.content() {
129 content
130 .iter()
131 .filter_map(|d| match d {
132 ContentDirectory::Media(lib) => match lib.library_type {
133 #[cfg(not(feature = "tests_deny_unknown_fields"))]
134 LibraryType::Unknown => None,
135 _ => Some(Library::new(self.client.clone(), *lib.clone())),
136 },
137 _ => None,
138 })
139 .collect()
140 } else {
141 Vec::new()
142 }
143 }
144
145 #[tracing::instrument(
150 name = "Server::transcode_artwork",
151 level = "debug",
152 skip(self, writer)
153 )]
154 pub async fn transcode_artwork<W>(
155 &self,
156 art: &str,
157 width: u32,
158 height: u32,
159 options: ArtTranscodeOptions,
160 writer: W,
161 ) -> Result<()>
162 where
163 W: AsyncWrite + Unpin,
164 {
165 transcode_artwork(&self.client, art, width, height, options, writer).await
166 }
167
168 #[tracing::instrument(level = "debug", skip(self))]
170 pub async fn transcode_sessions(&self) -> Result<Vec<TranscodeSession>> {
171 let wrapper: MediaContainerWrapper<TranscodeSessionsMediaContainer> =
172 self.client.get(SERVER_TRANSCODE_SESSIONS).json().await?;
173
174 Ok(wrapper
175 .media_container
176 .transcode_sessions
177 .into_iter()
178 .map(move |stats| TranscodeSession::from_stats(self.client.clone(), stats))
179 .collect())
180 }
181
182 #[tracing::instrument(level = "debug", skip(self))]
184 pub async fn transcode_session(&self, session_id: &str) -> Result<TranscodeSession> {
185 let stats = transcode_session_stats(&self.client, session_id).await?;
186 Ok(TranscodeSession::from_stats(self.client.clone(), stats))
187 }
188
189 #[tracing::instrument(level = "debug", skip(self))]
192 pub async fn item_by_id(&self, rating_key: &str) -> Result<Item> {
193 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");
194
195 match metadata_items(&self.client, &path).await {
196 Ok(items) => items.into_iter().next().ok_or(Error::ItemNotFound),
197 Err(Error::UnexpectedApiResponse {
198 status_code,
199 content,
200 }) => {
201 if status_code == 404 {
203 Err(Error::ItemNotFound)
204 } else {
205 Err(Error::UnexpectedApiResponse {
206 status_code,
207 content,
208 })
209 }
210 }
211 Err(err) => Err(err),
212 }
213 }
214
215 pub async fn mark_watched<M: MediaItem + FromMetadata>(&self, item: &M) -> Result<M> {
217 let rating_key = item.rating_key();
218 let path =
219 format!("{SERVER_SCROBBLE}?identifier=com.plexapp.plugins.library&key={rating_key}");
220
221 self.client.get(path).consume().await?;
222
223 let item = self.item_by_id(rating_key).await?;
224 Ok(M::from_metadata(
225 self.client.clone(),
226 item.metadata().clone(),
227 ))
228 }
229
230 pub async fn mark_unwatched<M: MediaItem + FromMetadata>(&self, item: &M) -> Result<M> {
232 let rating_key = item.rating_key();
233 let path =
234 format!("{SERVER_UNSCROBBLE}?identifier=com.plexapp.plugins.library&key={rating_key}");
235
236 self.client.get(path).consume().await?;
237
238 let item = self.item_by_id(rating_key).await?;
239 Ok(M::from_metadata(
240 self.client.clone(),
241 item.metadata().clone(),
242 ))
243 }
244
245 pub async fn update_timeline<M: MediaItem + FromMetadata>(
248 &self,
249 item: &M,
250 position: u64,
251 ) -> Result<M> {
252 let rating_key = item.rating_key();
253 let query = Query::new()
254 .param("key", format!("/library/metadata/{rating_key}"))
255 .param("ratingKey", rating_key)
256 .param("offline", "1")
257 .param("state", "playing")
258 .param("time", position.to_string());
259
260 self.client
261 .get(format!("{SERVER_TIMELINE}?{query}"))
262 .consume()
263 .await?;
264
265 let item = self.item_by_id(rating_key).await?;
266 Ok(M::from_metadata(
267 self.client.clone(),
268 item.metadata().clone(),
269 ))
270 }
271
272 #[tracing::instrument(level = "debug", skip(self))]
273 pub async fn refresh(self) -> Result<Self> {
274 Self::build(self.client, self.myplex_api_url).await
275 }
276
277 pub fn myplex(&self) -> Result<MyPlex> {
278 self.myplex_with_api_url(self.myplex_api_url.clone())
279 }
280
281 #[tracing::instrument(level = "debug", skip(self))]
282 pub async fn claim(self, claim_token: &str) -> Result<Self> {
283 let url = format!(
284 "{}?{}",
285 SERVER_MYPLEX_CLAIM,
286 serde_urlencoded::to_string([("token", claim_token)])?
287 );
288 let mut response = self.client.post(url).send().await?;
289
290 if response.status().as_http_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 #[tracing::instrument(level = "debug", skip(self))]
299 pub async fn unclaim(self) -> Result<Self> {
300 let mut response = self.client.delete(SERVER_MYPLEX_ACCOUNT).send().await?;
301
302 if response.status().as_http_status() == StatusCode::OK {
303 response.consume().await?;
304 self.refresh().await
305 } else {
306 Err(crate::Error::from_response(response).await)
307 }
308 }
309
310 pub fn myplex_with_api_url<U>(&self, api_url: U) -> Result<MyPlex>
311 where
312 Uri: TryFrom<U>,
313 <Uri as TryFrom<U>>::Error: Into<http::Error>,
314 {
315 Ok(MyPlex::new(
316 HttpClientBuilder::from(self.client.clone())
317 .set_api_url(api_url)
318 .build()?,
319 ))
320 }
321
322 pub fn client(&self) -> &HttpClient {
323 &self.client
324 }
325
326 pub async fn preferences<'a>(&self) -> Result<Preferences<'a>> {
327 Preferences::new(&self.client).await
328 }
329
330 pub fn machine_identifier(&self) -> &str {
331 &self.media_container.machine_identifier
332 }
333
334 pub async fn download_queue(&self) -> Result<DownloadQueue> {
335 DownloadQueue::get_or_create(self.client.clone()).await
336 }
337}