pub mod library;
pub(crate) mod prefs;
pub mod transcode;
use self::{
library::{metadata_items, FromMetadata, Item, Library, MediaItem, MetadataItem},
prefs::Preferences,
transcode::{
transcode_artwork, transcode_session_stats, ArtTranscodeOptions, TranscodeSession,
TranscodeSessionsMediaContainer,
},
};
#[cfg(not(feature = "tests_deny_unknown_fields"))]
use crate::media_container::server::library::LibraryType;
use crate::{
http_client::HttpClient,
media_container::{
server::{library::ContentDirectory, MediaProviderFeature, Server as ServerMediaContainer},
MediaContainerWrapper,
},
myplex::MyPlex,
url::{
SERVER_MEDIA_PROVIDERS, SERVER_MYPLEX_ACCOUNT, SERVER_MYPLEX_CLAIM, SERVER_SCROBBLE,
SERVER_TIMELINE, SERVER_TRANSCODE_SESSIONS, SERVER_UNSCROBBLE,
},
Error, HttpClientBuilder, Result,
};
use core::convert::TryFrom;
use futures::AsyncWrite;
use http::{StatusCode, Uri};
use isahc::AsyncReadResponseExt;
use std::{
collections::HashMap,
fmt::{self, Debug},
};
struct Query {
params: HashMap<String, String>,
}
impl Query {
fn new() -> Self {
Self {
params: HashMap::new(),
}
}
fn param<N: Into<String>, V: Into<String>>(mut self, name: N, value: V) -> Self {
self.params.insert(name.into(), value.into());
self
}
}
impl fmt::Display for Query {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(&serde_urlencoded::to_string(&self.params).unwrap())
}
}
#[derive(Debug, Clone)]
pub struct Server {
client: HttpClient,
pub myplex_api_url: Uri,
pub media_container: ServerMediaContainer,
}
impl Server {
async fn build(client: HttpClient, myplex_api_url: Uri) -> Result<Self> {
let media_container_wrapper: MediaContainerWrapper<ServerMediaContainer> =
client.get(SERVER_MEDIA_PROVIDERS).json().await?;
Ok(Self {
media_container: media_container_wrapper.media_container,
client,
myplex_api_url,
})
}
#[tracing::instrument(level = "debug", skip(client))]
pub async fn new<U>(url: U, client: HttpClient) -> Result<Self>
where
U: Debug,
Uri: TryFrom<U>,
<Uri as TryFrom<U>>::Error: Into<http::Error>,
{
let myplex_api_url = client.api_url.clone();
Self::build(
HttpClientBuilder::from(client).set_api_url(url).build()?,
myplex_api_url,
)
.await
}
fn content(&self) -> Option<&Vec<ContentDirectory>> {
if let Some(provider) = self
.media_container
.media_providers
.iter()
.find(|p| p.identifier == "com.plexapp.plugins.library")
{
for feature in &provider.features {
if let MediaProviderFeature::Content {
key: _,
ref directory,
} = feature
{
return Some(directory);
}
}
}
None
}
pub fn libraries(&self) -> Vec<Library> {
if let Some(content) = self.content() {
content
.iter()
.filter_map(|d| match d {
ContentDirectory::Media(lib) => match lib.library_type {
#[cfg(not(feature = "tests_deny_unknown_fields"))]
LibraryType::Unknown => None,
_ => Some(Library::new(self.client.clone(), *lib.clone())),
},
_ => None,
})
.collect()
} else {
Vec::new()
}
}
#[tracing::instrument(
name = "Server::transcode_artwork",
level = "debug",
skip(self, writer)
)]
pub async fn transcode_artwork<W>(
&self,
art: &str,
width: u32,
height: u32,
options: ArtTranscodeOptions,
writer: W,
) -> Result<()>
where
W: AsyncWrite + Unpin,
{
transcode_artwork(&self.client, art, width, height, options, writer).await
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn transcode_sessions(&self) -> Result<Vec<TranscodeSession>> {
let wrapper: MediaContainerWrapper<TranscodeSessionsMediaContainer> =
self.client.get(SERVER_TRANSCODE_SESSIONS).json().await?;
Ok(wrapper
.media_container
.transcode_sessions
.into_iter()
.map(move |stats| TranscodeSession::from_stats(self.client.clone(), stats))
.collect())
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn transcode_session(&self, session_id: &str) -> Result<TranscodeSession> {
let stats = transcode_session_stats(&self.client, session_id).await?;
Ok(TranscodeSession::from_stats(self.client.clone(), stats))
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn item_by_id(&self, rating_key: &str) -> Result<Item> {
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");
match metadata_items(&self.client, &path).await {
Ok(items) => items.into_iter().next().ok_or(Error::ItemNotFound),
Err(Error::UnexpectedApiResponse {
status_code,
content,
}) => {
if status_code == 404 {
Err(Error::ItemNotFound)
} else {
Err(Error::UnexpectedApiResponse {
status_code,
content,
})
}
}
Err(err) => Err(err),
}
}
pub async fn mark_watched<M: MediaItem + FromMetadata>(&self, item: &M) -> Result<M> {
let rating_key = item.rating_key();
let path =
format!("{SERVER_SCROBBLE}?identifier=com.plexapp.plugins.library&key={rating_key}");
self.client.get(path).consume().await?;
let item = self.item_by_id(rating_key).await?;
Ok(M::from_metadata(
self.client.clone(),
item.metadata().clone(),
))
}
pub async fn mark_unwatched<M: MediaItem + FromMetadata>(&self, item: &M) -> Result<M> {
let rating_key = item.rating_key();
let path =
format!("{SERVER_UNSCROBBLE}?identifier=com.plexapp.plugins.library&key={rating_key}");
self.client.get(path).consume().await?;
let item = self.item_by_id(rating_key).await?;
Ok(M::from_metadata(
self.client.clone(),
item.metadata().clone(),
))
}
pub async fn update_timeline<M: MediaItem + FromMetadata>(
&self,
item: &M,
position: u64,
) -> Result<M> {
let rating_key = item.rating_key();
let query = Query::new()
.param("key", format!("/library/metadata/{rating_key}"))
.param("ratingKey", rating_key)
.param("offline", "1")
.param("state", "playing")
.param("time", position.to_string());
self.client
.get(format!("{SERVER_TIMELINE}?{query}"))
.consume()
.await?;
let item = self.item_by_id(rating_key).await?;
Ok(M::from_metadata(
self.client.clone(),
item.metadata().clone(),
))
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn refresh(self) -> Result<Self> {
Self::build(self.client, self.myplex_api_url).await
}
pub fn myplex(&self) -> Result<MyPlex> {
self.myplex_with_api_url(self.myplex_api_url.clone())
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn claim(self, claim_token: &str) -> Result<Self> {
let url = format!(
"{}?{}",
SERVER_MYPLEX_CLAIM,
serde_urlencoded::to_string([("token", claim_token)])?
);
let mut response = self.client.post(url).send().await?;
if response.status() == StatusCode::OK {
response.consume().await?;
self.refresh().await
} else {
Err(crate::Error::from_response(response).await)
}
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn unclaim(self) -> Result<Self> {
let mut response = self.client.delete(SERVER_MYPLEX_ACCOUNT).send().await?;
if response.status() == StatusCode::OK {
response.consume().await?;
self.refresh().await
} else {
Err(crate::Error::from_response(response).await)
}
}
pub fn myplex_with_api_url<U>(&self, api_url: U) -> Result<MyPlex>
where
Uri: TryFrom<U>,
<Uri as TryFrom<U>>::Error: Into<http::Error>,
{
Ok(MyPlex::new(
HttpClientBuilder::from(self.client.clone())
.set_api_url(api_url)
.build()?,
))
}
pub fn client(&self) -> &HttpClient {
&self.client
}
pub async fn preferences<'a>(&self) -> Result<Preferences<'a>> {
Preferences::new(&self.client).await
}
pub fn machine_identifier(&self) -> &str {
&self.media_container.machine_identifier
}
}