Skip to content

Commit

Permalink
feat: add sharing and friends management
Browse files Browse the repository at this point in the history
  • Loading branch information
andrey-yantsen committed Mar 12, 2023
1 parent 06f0578 commit 974ecb1
Show file tree
Hide file tree
Showing 16 changed files with 712 additions and 77 deletions.
28 changes: 28 additions & 0 deletions crates/plex-api/examples/friend-accept.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use plex_api::{sharing::InviteStatus::PendingReceived, MyPlexBuilder};
use rpassword::prompt_password;
use std::io::{stdin, stdout, BufRead, Write};

#[async_std::main]
async fn main() {
let token = prompt_password("Token: ").unwrap();
print!("Friend's username or friendly_name to accept invitation: ");
stdout().flush().unwrap();

let username = stdin().lock().lines().next().unwrap().unwrap();

let myplex = MyPlexBuilder::default()
.set_token(token)
.build()
.await
.unwrap();

let friends = myplex.sharing().friends(PendingReceived).await.unwrap();
let friend = friends.into_iter().find(|friend| friend.title == username);

if let Some(friend) = friend {
friend.accept().await.unwrap();
println!("The invitation was accepts!");
} else {
eprintln!("Unable to find a friend with username '{username}'.");
}
}
50 changes: 50 additions & 0 deletions crates/plex-api/examples/friend-delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use plex_api::{sharing::InviteStatus, MyPlexBuilder};
use rpassword::prompt_password;
use std::io::{stdin, stdout, BufRead, Write};

#[async_std::main]
async fn main() {
let token = prompt_password("Token: ").unwrap();
print!("Friend's username or friendly_name to delete: ");
stdout().flush().unwrap();

let username = stdin().lock().lines().next().unwrap().unwrap();

let myplex = MyPlexBuilder::default()
.set_token(token)
.build()
.await
.unwrap();

let friends = myplex
.sharing()
.friends(InviteStatus::Accepted)
.await
.unwrap();
let mut friend = friends.into_iter().find(|friend| friend.title == username);

if friend.is_none() {
let friends = myplex
.sharing()
.friends(InviteStatus::PendingReceived)
.await
.unwrap();
friend = friends.into_iter().find(|friend| friend.title == username);
}

if friend.is_none() {
let friends = myplex
.sharing()
.friends(InviteStatus::PendingSent)
.await
.unwrap();
friend = friends.into_iter().find(|friend| friend.title == username);
}

if let Some(friend) = friend {
friend.delete().await.unwrap();
println!("The friend was deleted!");
} else {
eprintln!("Unable to find a friend with username '{username}'.");
}
}
26 changes: 26 additions & 0 deletions crates/plex-api/examples/friend-invite.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use plex_api::{sharing::User, MyPlexBuilder};
use rpassword::prompt_password;
use std::io::{stdin, stdout, BufRead, Write};

#[async_std::main]
async fn main() {
let token = prompt_password("Token: ").unwrap();
print!("Friend's username or email: ");
stdout().flush().unwrap();

let identifier = stdin().lock().lines().next().unwrap().unwrap();

let myplex = MyPlexBuilder::default()
.set_token(token)
.build()
.await
.unwrap();

let friend = myplex
.sharing()
.invite(User::UsernameOrEmail(&identifier))
.await
.unwrap();

println!("Invite sent! Friend id is {}", friend.id);
}
75 changes: 75 additions & 0 deletions crates/plex-api/examples/friend-share.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use plex_api::{
sharing::{Filters, Permissions, ShareableLibrary, ShareableServer, User},
MyPlexBuilder,
};
use rpassword::prompt_password;
use std::io::{stdin, stdout, BufRead, Write};

#[async_std::main]
async fn main() {
let token = prompt_password("Token: ").unwrap();
let myplex = MyPlexBuilder::default()
.set_token(token)
.build()
.await
.unwrap();

print!("Friend's username or email: ");
stdout().flush().unwrap();

let friend_identifier = stdin().lock().lines().next().unwrap().unwrap();

print!("Machine ID to share: ");
stdout().flush().unwrap();

let machine_identifier = stdin().lock().lines().next().unwrap().unwrap();

let server_info = myplex.server_info(&machine_identifier).await.unwrap();

if server_info.library_sections.is_empty() {
panic!("The server has no libraries");
}

println!();

println!("Server has the following libraries:");

for library in &server_info.library_sections {
println!("{}. {} ({})", library.key, library.title, library.r#type);
}

println!();

print!("Comma-separated library keys to share (or 'all'): ");
stdout().flush().unwrap();

let mut libraries = stdin().lock().lines().next().unwrap().unwrap();

if libraries == "all" {
libraries = server_info
.library_sections
.iter()
.map(|l| l.id.to_string())
.collect::<Vec<String>>()
.join(",");
}

let libraries: Vec<ShareableLibrary> = libraries
.split(',')
.map(|key| ShareableLibrary::LibraryId(key.trim()))
.collect();

let friend = myplex
.sharing()
.share(
User::UsernameOrEmail(&friend_identifier),
ShareableServer::MachineIdentifier(&machine_identifier),
&libraries,
Permissions::default(),
Filters::default(),
)
.await
.unwrap();

println!("Invite sent! Friend id is {}", friend.id);
}
2 changes: 2 additions & 0 deletions crates/plex-api/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ pub enum Error {
TranscodeError(String),
#[error("The server thinks the client should just play the original media.")]
TranscodeRefused,
#[error("Only invites with status pending_received can be accepted.")]
InviteAcceptingNotPendingReceived,
}

const PLEX_API_ERROR_CODE_AUTH_OTP_REQUIRED: i32 = 1029;
Expand Down
1 change: 1 addition & 0 deletions crates/plex-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub use media_container::server::library::{
AudioCodec, ContainerFormat, Decision, Protocol, SubtitleCodec, VideoCodec,
};
pub use media_container::server::Feature as ServerFeature;
pub use myplex::sharing;
pub use myplex::{device, pin::PinManager, MyPlex, MyPlexBuilder};
pub use player::Player;
pub use server::library::{
Expand Down
20 changes: 18 additions & 2 deletions crates/plex-api/src/myplex/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ use crate::{
Error, Player, Result, Server,
};
use futures::{future::select_ok, FutureExt};
use http::StatusCode;
use isahc::AsyncReadResponseExt;
use secrecy::ExposeSecret;

pub struct DeviceManager {
pub client: Arc<HttpClient>,
Expand Down Expand Up @@ -57,13 +56,30 @@ impl Device<'_> {
self.inner.provides.contains(&feature)
}

pub fn identifier(&self) -> &str {
&self.inner.client_identifier
}

pub fn access_token(&self) -> Option<&str> {
self.inner
.access_token
.as_ref()
.map(|v| v.expose_secret().as_str())
}

pub async fn connect(&self) -> Result<DeviceConnection> {
if !self.inner.provides.contains(&Feature::Server)
&& !self.inner.provides.contains(&Feature::Player)
{
return Err(Error::DeviceConnectionNotSupported);
}

if let Some(access_token) = self.inner.access_token.as_ref() {
if access_token.expose_secret() != self.client.x_plex_token() {
todo!("connection to a shared device is not implemented");
}
}

if !self.inner.connections.is_empty() {
if self.inner.provides.contains(&Feature::Server) {
let futures = self
Expand Down
2 changes: 1 addition & 1 deletion crates/plex-api/src/myplex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl MyPlex {
}

pub fn sharing(&self) -> Sharing {
Sharing::new(self.client.clone())
Sharing::new(self)
}

pub async fn server_info(&self, machine_identifier: &str) -> Result<server::ServerInfo> {
Expand Down
101 changes: 101 additions & 0 deletions crates/plex-api/src/myplex/sharing/friend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use crate::url::MYPLEX_INVITES_FRIENDS;
use crate::Error;
use crate::HttpClient;
use crate::Result;
use http::StatusCode;
use isahc::AsyncReadResponseExt;
use serde::{Deserialize, Serialize};
use serde_plain::derive_display_from_serialize;

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum InviteStatus {
PendingSent,
Accepted,
PendingReceived,
Pending,
#[cfg(not(feature = "tests_deny_unknown_fields"))]
#[serde(other)]
Unknown,
}

derive_display_from_serialize!(InviteStatus);

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
pub struct Friend {
/// The global Plex.tv ID of the friend
pub id: u64,
/// Plex.tv user's uuid ??
pub uuid: String,
/// How the user should be displayed in the interface.
/// Seems to be generated on the backed from either username or friendly_name.
pub title: String,
/// Non-managed user's username
pub username: Option<String>,
/// Non-managed user's email
pub email: Option<String>,
/// Is it a managed user?
pub restricted: bool,
/// Managed user's username
pub friendly_name: Option<String>,
/// User's avatar picture URL
#[serde(with = "http_serde::uri")]
pub thumb: http::Uri,
/// Does the user has access to Plex Home?
pub home: bool,
/// Status of the friendship
pub status: Option<InviteStatus>,
/// Server sharing preferences
pub sharing_settings: Option<super::server::Settings>,
/// List of the shared servers
#[serde(default)]
pub shared_servers: Vec<super::server::SharedServer>,
#[serde(skip)]
/// No idea what this is
pub shared_sources: Vec<String>,
#[serde(skip)]
pub(crate) client: Option<HttpClient>,
}

impl Friend {
pub async fn accept(&self) -> Result<Friend> {
if !matches!(
self.status,
Some(InviteStatus::PendingReceived) | Some(InviteStatus::Pending)
) {
return Err(Error::InviteAcceptingNotPendingReceived);
}

let mut friend: Friend = self
.client()
.post(format!("{}/{}/accept", MYPLEX_INVITES_FRIENDS, self.id))
.json()
.await?;
friend.client = self.client.clone();
Ok(friend)
}

/// Shorthand for self.client.as_ref().unwrap(). Unwrap must be safe at this stage
/// since the client must be set for a valid invite.
fn client(&self) -> &HttpClient {
self.client.as_ref().unwrap()
}

pub async fn delete(self) -> Result<()> {
let mut response = self
.client()
.delete(format!("{}/{}", MYPLEX_INVITES_FRIENDS, self.id))
.send()
.await?;

match response.status() {
StatusCode::OK | StatusCode::NO_CONTENT => {
response.consume().await?;
Ok(())
}
_ => Err(Error::from_response(response).await),
}
}
}
Loading

0 comments on commit 974ecb1

Please sign in to comment.