plex-api 0.0.12

Library for communication with Plex server. Work in progress, not ready for any use. See github for details.
Documentation
mod fixtures;

mod offline {
    use super::fixtures::offline::{myplex::*, Mocked};
    use httpmock::{
        Method::{DELETE, GET, POST},
        Mock, MockServer,
    };
    use plex_api::{
        sharing::{Filters, InviteStatus, Permissions, ShareableLibrary, User},
        url::{
            MYPLEX_INVITES_FRIENDS, MYPLEX_INVITES_INVITE, MYPLEX_INVITES_SHARED_SERVERS,
            MYPLEX_SERVERS,
        },
        MyPlex, RestrictionProfile,
    };

    fn prepare_friends_mock<'a>(
        mock_server: &'a MockServer,
        status: &'a str,
        mock_file: &'a str,
    ) -> Mock<'a> {
        mock_server.mock(|when, then| {
            when.method(GET)
                .path(MYPLEX_INVITES_FRIENDS)
                .query_param("includeSharedServers", "true")
                .query_param("includeSharedSources", "true")
                .query_param("includeSharingSettings", "true")
                .query_param("status", status);
            then.status(200)
                .header("content-type", "application/json")
                .body_from_file(mock_file);
        })
    }

    #[plex_api_test_helper::offline_test]
    #[case::no_friends("tests/mocks/myplex/api/v2/friends_empty.json", 0)]
    #[case::single_managed_user(
        "tests/mocks/myplex/api/v2/friends_accepted_one_restricted.json",
        1
    )]
    #[case::four_managed_users(
        "tests/mocks/myplex/api/v2/friends_accepted_four_restricted.json",
        4
    )]
    #[case::single_external_user("tests/mocks/myplex/api/v2/friends_accepted_one_external.json", 1)]
    #[case::managed_and_external_user("tests/mocks/myplex/api/v2/friends_accepted_mixed.json", 2)]
    async fn list_friends(
        #[future] myplex: Mocked<MyPlex>,
        #[case] mock_file: &str,
        #[case] friends_count: usize,
    ) {
        let (myplex, mock_server) = myplex.split();
        let sharing = myplex.sharing().unwrap();

        let m = prepare_friends_mock(&mock_server, "accepted", mock_file);

        assert_eq!(
            friends_count,
            sharing.friends(InviteStatus::Accepted).await.unwrap().len()
        );

        m.assert();
    }

    #[plex_api_test_helper::offline_test]
    async fn filters_deserialization(#[future] myplex: Mocked<MyPlex>) {
        let (myplex, mock_server) = myplex.split();
        let sharing = myplex.sharing().unwrap();

        let mut m = prepare_friends_mock(
            &mock_server,
            "accepted",
            "tests/mocks/myplex/api/v2/friends_accepted_four_restricted.json",
        );
        let friends = sharing.friends(InviteStatus::Accepted).await.unwrap();
        m.assert();
        m.delete();

        let mut found_adult = false;
        let mut found_little_kid = false;
        let mut found_teen = false;
        let mut found_older_kid = false;

        for friend in friends {
            let filter_movies = friend
                .sharing_settings
                .as_ref()
                .unwrap()
                .filter_movies
                .as_ref()
                .unwrap();

            match &friend.restriction_profile {
                None => {
                    found_adult = true;

                    assert_eq!(vec!["12"], filter_movies.content_rating);
                    assert_eq!(vec!["Approved"], filter_movies.exclude_content_rating);
                    assert_eq!(
                        vec!["BAFTA", "1001 Movies You Must See Before You Die"],
                        filter_movies.label
                    );
                    assert!(filter_movies.exclude_label.is_empty());
                }
                Some(RestrictionProfile::LittleKid) => {
                    found_little_kid = true;

                    assert!(filter_movies.content_rating.iter().any(|r| r == "US/TV-Y"));
                    assert!(filter_movies.content_rating.iter().any(|r| r == "TV-G"));
                    assert!(!filter_movies.content_rating.iter().any(|r| r == "US/PG"));
                    assert!(!filter_movies.content_rating.iter().any(|r| r == "US/PG-13"));
                    assert!(!filter_movies.content_rating.iter().any(|r| r == "US/TV-Y7"));
                    assert!(filter_movies.exclude_content_rating.is_empty());
                    assert!(filter_movies.label.is_empty());
                    assert!(filter_movies.exclude_label.is_empty());
                }
                Some(RestrictionProfile::OlderKid) => {
                    found_older_kid = true;

                    assert!(filter_movies.content_rating.iter().any(|r| r == "US/TV-Y"));
                    assert!(filter_movies.content_rating.iter().any(|r| r == "US/TV-Y7"));
                    assert!(filter_movies.content_rating.iter().any(|r| r == "US/TV-PG"));
                    assert!(filter_movies.content_rating.iter().any(|r| r == "TV-G"));
                    assert!(!filter_movies.content_rating.iter().any(|r| r == "US/PG-13"));
                    assert!(filter_movies.exclude_content_rating.is_empty());
                    assert!(filter_movies.label.is_empty());
                    assert!(filter_movies.exclude_label.is_empty());
                }
                Some(RestrictionProfile::Teen) => {
                    found_teen = true;

                    assert!(filter_movies.content_rating.iter().any(|r| r == "US/TV-Y"));
                    assert!(filter_movies.content_rating.iter().any(|r| r == "US/TV-Y7"));
                    assert!(filter_movies.content_rating.iter().any(|r| r == "US/TV-PG"));
                    assert!(filter_movies.content_rating.iter().any(|r| r == "TV-G"));
                    assert!(filter_movies.content_rating.iter().any(|r| r == "US/PG-13"));
                    assert!(filter_movies.exclude_content_rating.is_empty());
                    assert!(filter_movies.label.is_empty());
                    assert!(filter_movies.exclude_label.is_empty());
                }
                #[cfg(not(feature = "tests_deny_unknown_fields"))]
                _ => {
                    panic!("Unexpected friend");
                }
            }
        }

        assert!(
            found_adult,
            "'Adult' friend wasn't found in the friends list"
        );
        assert!(
            found_little_kid,
            "'Little kid' friend wasn't found in the friends list"
        );
        assert!(found_teen, "'Teen' friend wasn't found in the friends list");
        assert!(
            found_older_kid,
            "'Older kid' friend wasn't found in the friends list"
        );
    }

    #[plex_api_test_helper::offline_test]
    async fn invite(#[future] myplex: Mocked<MyPlex>) {
        let (myplex, mock_server) = myplex.split();
        let sharing = myplex.sharing().unwrap();

        let m = mock_server.mock(|when, then| {
            when.method(POST)
                .path(MYPLEX_INVITES_INVITE)
                .query_param("identifier", "user");
            then.status(201)
                .header("content-type", "application/json")
                .body_from_file("tests/mocks/myplex/api/v2/friends/invite_create.json");
        });

        let friend = sharing
            .invite(plex_api::sharing::User::UsernameOrEmail("user"))
            .await
            .unwrap();

        m.assert();

        assert_eq!(1, friend.id);
        assert_eq!("deadbeef", friend.uuid);
        assert_eq!("username", friend.title);
        assert_eq!("username", friend.username.unwrap());
        assert!(!friend.restricted);
        assert_eq!(None, friend.friendly_name);
        assert_eq!(
            "https://plex.tv/users/deadbeef/avatar?c=23423423",
            friend.thumb
        );
        assert!(!friend.home);
        assert!(matches!(friend.status, Some(InviteStatus::Pending)));
    }

    #[plex_api_test_helper::offline_test]
    async fn share_server(#[future] myplex: Mocked<MyPlex>) {
        let (myplex, mock_server) = myplex.split();
        let sharing = myplex.sharing().unwrap();

        let server_info_mock = mock_server.mock(|when, then| {
            when.method(GET)
                .path(format!("{MYPLEX_SERVERS}/machine_id"));
            then.status(201)
                .header("content-type", "application/json")
                .body_from_file("tests/mocks/myplex/api/v2/servers/machine_id.json");
        });

        let share_create_mock = mock_server.mock(|when, then| {
            when.method(POST)
                .path(MYPLEX_INVITES_SHARED_SERVERS)
                .json_body(serde_json::json!({
                    "invitedEmail": "user",
                    "librarySectionIds": [1001, 1002],
                    "settings": {
                        "allowChannels": true,
                        "allowSubtitleAdmin": true,
                        "allowSync": true,
                        "allowCameraUpload": false,
                        "allowTuners": 0,
                        "filterMovies": "",
                        "filterMusic": "",
                        "filterTelevision": "",
                        "filterPhotos": null,
                        "filterAll": null
                    },
                    "machineIdentifier": "machine_id"
                }));
            then.status(201)
                .header("content-type", "application/json")
                .body_from_file("tests/mocks/myplex/api/v2/shared_server_create.json");
        });

        let share = sharing
            .share(
                User::UsernameOrEmail("user"),
                plex_api::sharing::ShareableServer::MachineIdentifier("machine_id"),
                &[
                    ShareableLibrary::LibraryId("1"),
                    ShareableLibrary::LibraryId("2"),
                ],
                Permissions::default(),
                Filters::default(),
            )
            .await
            .unwrap();

        server_info_mock.assert();
        share_create_mock.assert();

        assert_eq!("deadbeef1", share.invited.unwrap().uuid);
    }

    #[plex_api_test_helper::offline_test]
    async fn accept_not_pending_forbidden(#[future] myplex: Mocked<MyPlex>) {
        let (myplex, mock_server) = myplex.split();
        let sharing = myplex.sharing().unwrap();

        let mut m = prepare_friends_mock(
            &mock_server,
            "accepted",
            "tests/mocks/myplex/api/v2/friends_accepted_one_external.json",
        );
        let mut friends = sharing.friends(InviteStatus::Accepted).await.unwrap();
        m.assert();
        m.delete();

        let friend = friends.pop().unwrap();

        let err = friend.accept().await.unwrap_err();
        assert!(
            matches!(err, plex_api::Error::InviteAcceptingNotPendingReceived),
            "Unexpected errors returned by Friend::accept()"
        );
    }

    #[plex_api_test_helper::offline_test]
    async fn accept_and_reject(#[future] myplex: Mocked<MyPlex>) {
        let (myplex, mock_server) = myplex.split();
        let sharing = myplex.sharing().unwrap();

        let mut m = prepare_friends_mock(
            &mock_server,
            "pending_received",
            "tests/mocks/myplex/api/v2/friends_pending_received_one_external.json",
        );
        let mut friends = sharing
            .friends(InviteStatus::PendingReceived)
            .await
            .unwrap();
        m.assert();
        m.delete();

        let friend = friends.pop().unwrap();

        let mock_delete = mock_server.mock(|when, then| {
            when.method(DELETE)
                .path(format!("{MYPLEX_INVITES_FRIENDS}/{}", friend.id));
            then.status(200).header("content-type", "application/json");
        });

        let del = friend.delete().await;
        mock_delete.assert();
        del.unwrap();

        let mut m = prepare_friends_mock(
            &mock_server,
            "pending_received",
            "tests/mocks/myplex/api/v2/friends_pending_received_one_external.json",
        );
        let friends = sharing.friends(InviteStatus::PendingReceived).await;
        m.assert();
        m.delete();

        let mut friends = friends.unwrap();

        let friend = friends.pop().unwrap();

        let mock_accept = mock_server.mock(|when, then| {
            when.method(POST)
                .path(format!("{MYPLEX_INVITES_FRIENDS}/{}/accept", friend.id));
            then.status(200)
                .header("content-type", "application/json")
                .body_from_file("tests/mocks/myplex/api/v2/friends/accept.json");
        });

        let friend = friend.accept().await;
        mock_accept.assert();
        let friend = friend.unwrap();

        assert!(
            matches!(friend.status, Some(InviteStatus::Accepted)),
            "Unexpected friend status"
        );
    }
}

mod online {
    use super::fixtures::online::myplex;
    use plex_api::{sharing::InviteStatus, MyPlex};

    #[plex_api_test_helper::online_test_myplex]
    async fn list_friends(#[future] myplex: MyPlex) {
        myplex
            .sharing()
            .unwrap()
            .friends(InviteStatus::Accepted)
            .await
            .unwrap();
    }
}