@ctrl/plex
    Preparing search index...

    @ctrl/plex

    @ctrl/plex npm

    A TypeScript Plex API client using ofetch based on pkkid/python-plexapi

    npm install @ctrl/plex
    

    https://plex.ep.workers.dev

    Create a plex connection

    import { MyPlexAccount } from '@ctrl/plex';

    const account = await new MyPlexAccount({
    baseUrl: 'http://localhost:32400',
    // Use username and password based auth
    username: 'username',
    password: 'password',
    // OR Token based auth
    token: 'token',
    }).connect();

    // List all available servers (resources) attached to your account
    const resources = await account.resources();

    // Connect to a specific server by name
    // You can find the server name in the Plex Web UI settings or by listing resources above
    const resource = await account.resource('<SERVERNAME>');
    const plex = await resource.connect();
    const library = await plex.library();

    // List all libraries (Movies, TV Shows, Music, etc.)
    const sections = await library.sections();

    Identify your app. Plex lists every client by its product name and client identifier, so set your own before connecting and persist the identifier so Plex keeps seeing the same device.

    import { setPlexHeaders } from '@ctrl/plex';

    setPlexHeaders({
    product: 'My App',
    version: '1.0.0',
    identifier: 'replace-with-a-persisted-client-identifier',
    });

    Authenticate without collecting a Plex username or password. Persist the client identifier so it stays stable across logins.

    import { MyPlexAccount, MyPlexPinLogin } from '@ctrl/plex';

    const clientIdentifier = 'replace-with-a-persisted-client-identifier';
    const login = await MyPlexPinLogin.create({ clientIdentifier, mode: 'oauth' });

    console.log(`Open ${login.oauthUrl()} to sign in`);

    const authentication = await login.wait({ timeout: 120_000 });
    const account = await new MyPlexAccount({ token: authentication.token }).connect();

    Create and refresh a signed Plex JWT. The bootstrap token must have been issued for the same client identifier. Store the returned credentials securely; they contain the private signing key.

    import { MyPlexAccount, refreshPlexJwt, registerPlexJwt } from '@ctrl/plex';

    let credentials = await registerPlexJwt({
    token: authentication.token,
    clientIdentifier,
    scopes: ['username', 'email', 'friendly_name'],
    });

    credentials = await refreshPlexJwt({ credentials });
    const jwtAccount = await new MyPlexAccount({ token: credentials.token }).connect();
    import { MovieSection } from '@ctrl/plex';

    // Pass MovieSection generic because the section title doesn't imply a section type.
    const section = await library.section<MovieSection>('Movies');
    // Get an array of Movie objects
    const results = await section.search({ unwatched: true });
    const library = await plex.library();
    const section = await library.section<MovieSection>('Movies');
    const results = await section.search({ title: 'Rush Hour' });
    const hubs = await plex.search('Arnold');
    // Each hub is one category of results (movies, episodes, actors, ...)
    for (const hub of hubs) {
    const [first] = await hub.items();
    console.log(hub.title, first?.title);
    }
    import { ShowSection } from '@ctrl/plex';

    // Pass ShowSection generic because the section title doesn't imply a section type.
    const section = await library.section<ShowSection>('TV Shows');
    // Get an array of Show objects
    const results = await section.search({ title: 'Silicon Valley' });
    const episodes = await results[0].episodes();
    import { Playlist } from '@ctrl/plex';

    // The ten highest rated movies
    const topMovies = await section.search({ sort: 'rating:desc', maxresults: 10 });

    // Create a new playlist
    const bestMovies = await Playlist.create(plex, 'Best Movies', { items: topMovies });

    v8 is a large release. The full list is in the release notes, the changes most likely to affect you:

    • Node.js 24 or newer is required.
    • Types are strict: fields Plex can omit are optional, and PlexServer.query() / MyPlexAccount.query() return unknown unless you pass a type argument.
    • Positional arguments became options objects, for example connect({ timeout }), removeItem(item, { refresh }), updateProgress({ time }) and account.device({ name }).
    • PlexClient commands take { type } instead of a positional mtype, and proxyThroughServer({ server }) replaces the _server / _proxyThroughServer fields.
    • Music sections list artists by default and onDeck() returns episodes and tracks.
    • Audio art and thumb are server paths, use artUrl / thumbUrl for token URLs.
    • plex.tv devices come from the JSON API, MyPlexDevice.id is a number.

    JS is a different language and some methods of the api were not possible. Chaining functions with requests must be awaited mostly individually. Constructors in JS don't typically make requests and accessing properties normally cannot make requests either. Methods take options objects instead of python style keyword arguments, and responses are requested as JSON.

    Tests are run against a real instance of plex.

    Create a plex account just for testing, using a real account will break everything. Put its credentials in a .env file at the root of the repo, the tests and scripts load it automatically.

    PLEX_USERNAME=email
    PLEX_PASSWORD=password
    PLEXAPI_AUTH_SERVER_TOKEN=token

    Setup test content (once)

    pnpm run add-media
    

    Run tests

    pnpm test
    

    Post testing, remove plex server from account. Warning this is destructive. Do not use this on a real plex account.

    pnpm run test-cleanup
    

    get a claim token from https://www.plex.tv/claim/ export PLEX_CLAIM_TOKEN=claim-token

    Start the plex container for testing. Replace /Users/scooper/gh/plex with the path to this repo's directory.

    docker run -d \
    --name=plex \
    --net=host \
    -h orbstack \
    -p 32400:32400/tcp \
    -p 32400:32400 \
    -p 1900:1900/udp \
    -p 5353:5353/udp \
    -p 8324:8324 \
    -p 32410:32410/udp \
    -p 32412:32412/udp \
    -p 32413:32413/udp \
    -p 32414:32414/udp \
    -p 32469:32469 \
    -e PUID=1000 \
    -e PGID=1000 \
    -e TZ=Etc/UTC \
    -e VERSION=docker \
    -e PLEX_CLAIM=$PLEX_CLAIM_TOKEN \
    -v /Users/scooper/gh/plex/plex/media/db:/config \
    -v /Users/scooper/gh/plex/plex/media/transcode:/transcode \
    -v /Users/scooper/gh/plex/plex/media:/data \
    --restart unless-stopped \
    lscr.io/linuxserver/plex:latest

    Pull latest plex container if needed

    docker pull lscr.io/linuxserver/plex:latest
    

    bootstrap plex server with test media. This uses PLEXAPI_AUTH_SERVER_TOKEN from .env.

    pnpm run bootstraptest -- --no-docker --server-name=orbstack