Skip to content

Instantly share code, notes, and snippets.

@philipjewell
Last active April 18, 2024 13:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save philipjewell/2b721ccde6f251f67454dd04829cef4b to your computer and use it in GitHub Desktop.
Save philipjewell/2b721ccde6f251f67454dd04829cef4b to your computer and use it in GitHub Desktop.
Plex Client Identifier

Plex Client Identifier

Cli based tool that assists with collecting client id's for the various devices of a given plex user/account.

Going down the rabbit hole of various automations Plex Webhooks has to offer, I came across webhooks-notifications. The installation steps included getting an auth token via POST request to their API as well as another GET request to fetch the clients under your account.

There were various posts on this forum of people showing their methods of gathering this information; however, the website isn't designed for code/snippet sharing. This caused problems for me with fancy quote, not being completely certain the keys vs the values in the example requests given, etc. So, whats better than just writing one out myself?

Initially I thought that it would be as easy as doing it all in a single curl - which I wasn't wrong about:

$ curl https://plex.tv/api/resources.xml?auth_token=$(curl -sX POST https://plex.tv/users/sign_in.json -u $USER:$PASS -H "X-Plex-Client-Identifier: da token" -H "X-Plex-Product: some more" -H "X-Plex-Version: 0.001" | jq -r .[].'authToken')

Seeing the XML dumped out into my terminal session made it super hard to visually parse out where the clientIdentifier was and if it was the right device.

This tool, will parse though all that and make it nice and neat with the following output:

Device name: ServerName1
	product: Plex Media Server
	platform: Windows
	clientIdentifier: <client-id-placeholder>
	createdAt: 2019-09-25 18:11:49
	lastSeenAt: 2020-02-17 14:39:18

Device name: AFTT
	product: Plex for Android (TV)
	platform: Android
	clientIdentifier: <client-id-placeholder>
	createdAt: 2018-07-11 16:47:48
	lastSeenAt: 2020-02-16 16:28:59

Device name: Philip’s iPhone
	product: Plex for iOS
	platform: iOS
	clientIdentifier: <client-id-placeholder>
	createdAt: 2019-01-07 23:13:53
	lastSeenAt: 2020-02-17 17:08:56

So, this tool will do the following for you:

  • Prompt you for your Plex username/password
  • Store your Plex username/password in your keyring
  • Get Plex API auth token
  • Get client XML and parse to be readable.

Usage

This script is made simple enough, that the only optional thing is the verbosity - which prints the response from the Plex API + your API token.

$ python3 ./plex_client_identifier.py --help
Usage: plex_client_identifier.py [OPTIONS]

  Main entry point for python Plex client identifier.

  :param: verbose: bool, print more verbose details

Options:
  -v, --verbose
  --help         Show this message and exit.
#!/usr/bin/env python3
"""Cli tool to assist gather parse client id's for a given Plex user."""
import base64
import datetime
import hashlib
import json
from collections import OrderedDict
from getpass import getpass
import click
import http.client
import keyring
import xmltodict
PLEX_HOSTNAME = 'plex.tv'
PLEX_SIGN_IN_URI = '/users/sign_in.json'
PLEX_API_RESOURCE_URI = '/api/resources.xml'
class PlexHelper:
"""Plex Helper class."""
def __init__(self, hostname=PLEX_HOSTNAME, sign_in_uri=PLEX_SIGN_IN_URI, resource_uri=PLEX_API_RESOURCE_URI, version=None, verbose=False):
"""Init the helper class."""
self.hostname = hostname
self.sign_in_uri = sign_in_uri
self.resource_uri = resource_uri
self.version = version if version else '0.001'
self.verbose = verbose
self.client_name = 'Client Name: Gathering Auth Token'
self.client_version = f'Client Version: {self.version}'
self.client_id = hashlib.sha512(f'{self.client_name} {self.client_version}'.encode()).hexdigest()
self.auth_token = self.get_auth_token()
self.filtered_keys = ['@product', '@platform', '@clientIdentifier']
self.date_specs = ['@createdAt', '@lastSeenAt']
def set_plex_credentials(self):
"""Set your Plex credentials in your keychain.
:return: str plex_username, str plex_password
"""
plex_username = getpass.getpass('Please enter your Plex username: ')
plex_password = getpass.getpass('Please enter your Plex username: ')
keyring.set_password('plex', 'plex_username', plex_username)
keyring.set_password('plex', plex_username, plex_password)
return plex_username, plex_password
def get_plex_credentials(self):
"""Gather Plex credentials if stored in keyring, otherwise prompt for them.
:return: str plex_username, str plex_password
"""
plex_username = keyring.get_password('plex', 'plex_username')
plex_password = keyring.get_password('plex', plex_username)
if not plex_username:
plex_username, plex_password = self.set_plex_credentials()
return plex_username, plex_password
def get_auth_token(self):
"""Get Plex auth token.
:return: str, auth_token
"""
username, password = self.get_plex_credentials()
base64string = base64.b64encode(f'{username}:{password}'.encode())
headers = {'Authorization': f'Basic {base64string.decode("ascii")}',
'X-Plex-Client-Identifier': self.client_id,
'X-Plex-Product': self.client_name,
'X-Plex-Version': self.client_version}
conn = http.client.HTTPSConnection(self.hostname)
conn.request('POST', self.sign_in_uri, '', headers)
response = conn.getresponse()
data = json.loads(response.read().decode())
auth_token = data['user']['authentication_token']
if self.verbose:
print(f'HTTP request: {self.hostname}{self.sign_in_uri}')
print(f'status: {response.status}, reason: {response.reason}')
print(f'Auth-Token: {auth_token}')
conn.close()
return auth_token
def get_api_resouces(self):
"""Get the Plex api resources per given token.
:return: list, device resource list
"""
conn = http.client.HTTPSConnection(self.hostname)
conn.request('GET', f'{self.resource_uri}?auth_token={self.auth_token}')
response = conn.getresponse()
xml_data = response.read().decode()
xml_dict = xmltodict.parse(xml_data)
json_dump = json.dumps(xml_dict)
conn.close()
return json.loads(json_dump)['MediaContainer']['Device']
def get_filtered_resources(self):
"""Filter out the resources returned from api.
:return: dict
"""
resource_list = self.get_api_resouces()
device_dict = {}
for device in resource_list:
device_dict[device.get('@name')] = {}
for spec, v in device.items():
if spec in self.filtered_keys:
device_dict[device.get('@name')][str(spec)] = str(v)
if spec in self.date_specs:
parsed_date = datetime.datetime.fromtimestamp(int(v)).strftime('%Y-%m-%d %H:%M:%S')
device_dict[device.get('@name')][str(spec)] = str(parsed_date)
return device_dict
@click.command()
@click.option('-v', '--verbose', is_flag=True)
def main(verbose):
"""Main entry point for python Plex client identifier.
:param: verbose: bool, print more verbose details
"""
plex_helper = PlexHelper(verbose=verbose)
devices = plex_helper.get_filtered_resources()
for name, device_details in devices.items():
print(f'Device name: {name}')
for k, v in device_details.items():
print(f'\t{k.split("@")[1]}: {v}')
print()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment